pax_global_header00006660000000000000000000000064150417021520014507gustar00rootroot0000000000000052 comment=196abef5486156c7822b9f8587081626cacd8b58 tablewriter-1.0.9/000077500000000000000000000000001504170215200140425ustar00rootroot00000000000000tablewriter-1.0.9/.github/000077500000000000000000000000001504170215200154025ustar00rootroot00000000000000tablewriter-1.0.9/.github/workflows/000077500000000000000000000000001504170215200174375ustar00rootroot00000000000000tablewriter-1.0.9/.github/workflows/go.yml000066400000000000000000000010171504170215200205660ustar00rootroot00000000000000# This workflow will build a golang project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go name: Go on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Build run: go build -v ./... - name: Test run: go test -v ./... tablewriter-1.0.9/.gitignore000066400000000000000000000000751504170215200160340ustar00rootroot00000000000000 # folders .idea .vscode /tmp /lab dev.sh *csv2table _test/ tablewriter-1.0.9/LICENSE.md000066400000000000000000000020421504170215200154440ustar00rootroot00000000000000Copyright (C) 2014 by Oleku Konko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tablewriter-1.0.9/MIGRATION.md000066400000000000000000004606761504170215200157400ustar00rootroot00000000000000# Migration Guide: tablewriter v0.0.5 to v1.0.x > **NOTE:** This document is work in progress, use with `caution`. This document is a comprehensive guide. For specific issues or advanced scenarios, please refer to the source code or open an issue. The `tablewriter` library has undergone a significant redesign between versions **v0.0.5** and **v1.0.x**, transitioning from a primarily method-driven API to a more robust, modular, and configuration-driven framework. This guide provides a detailed roadmap for migrating your v0.0.5 codebase to v1.0.x. It includes mappings of old methods to new approaches, practical examples, and explanations of new features. We believe these changes significantly improve the library's flexibility, maintainability, and power, enabling new features and making complex table configurations more manageable. ## Why Migrate to v1.0.x? The v1.0.x redesign enhances `tablewriter`’s flexibility, maintainability, and feature set: - **Extensibility**: Decoupled rendering supports diverse output formats (e.g., HTML, Markdown, CSV). - **Robust Configuration**: Centralized `Config` struct and fluent builders ensure atomic, predictable setups. - **Streaming Capability**: Dedicated API for row-by-row rendering, ideal for large or real-time datasets. - **Type Safety**: Specific types (e.g., `tw.State`, `tw.Align`) reduce errors and improve clarity. - **Consistent API**: Unified interface for intuitive usage across simple and complex use cases. - **New Features**: Hierarchical merging, granular padding, table captions, and fixed column widths. These improvements make v1.0.x more powerful, but they require updating code to leverage the new configuration-driven framework and take advantage of advanced functionalities. ## Key New Features in v1.0.x - **Fluent Configuration Builders**: `NewConfigBuilder()` enables chained, readable setups (`config.go:NewConfigBuilder`). - **Centralized Configuration**: `Config` struct governs table behavior and data processing (`config.go:Config`). - **Decoupled Renderer**: `tw.Renderer` interface with `tw.Rendition` for visual styling, allowing custom renderers (`tw/renderer.go`). - **True Streaming Support**: `Start()`, `Append()`, `Close()` for incremental rendering (`stream.go`). - **Hierarchical Cell Merging**: `tw.MergeHierarchical` for complex data structures (`tw/tw.go:MergeMode` constant, logic in `zoo.go`). - **Granular Padding Control**: Per-side (`Top`, `Bottom`, `Left`, `Right`) and per-column padding (`tw/cell.go:CellPadding`, `tw/types.go:Padding`). - **Enhanced Type System**: `tw.State`, `tw.Align`, `tw.Spot`, and others for clarity and safety (`tw/state.go`, `tw/types.go`). - **Comprehensive Error Handling**: Methods like `Render()` and `Append()` return errors (`tablewriter.go`, `stream.go`). - **Fixed Column Width System**: `Config.Widths` for precise column sizing, especially in streaming (`config.go:Config`, `tw/cell.go:CellWidth`). - **Table Captioning**: Flexible placement and styling with `tw.Caption` (`tw/types.go:Caption`). - **Advanced Data Processing**: Support for `tw.Formatter`, per-column filters, and stringer caching (`tw/cell.go:CellFilter`, `tablewriter.go:WithStringer`). ## Core Philosophy Changes in v1.0.x Understanding these shifts is essential for a successful migration: 1. **Configuration-Driven Approach**: - **Old**: Relied on `table.SetXxx()` methods for incremental, stateful modifications to table properties. - **New**: Table behavior is defined by a `tablewriter.Config` struct (`config.go:Config`), while visual styling is managed by a `tw.Rendition` struct (`tw/renderer.go:Rendition`). These are typically set at table creation using `NewTable()` with `Option` functions or via a fluent `ConfigBuilder`, ensuring atomic and predictable configuration changes. 2. **Decoupled Rendering Engine**: - **Old**: Rendering logic was tightly integrated into the `Table` struct, limiting output flexibility. - **New**: The `tw.Renderer` interface (`tw/renderer.go:Renderer`) defines rendering logic, with `renderer.NewBlueprint()` as the default text-based renderer. The renderer’s appearance (e.g., borders, symbols) is controlled by `tw.Rendition`, enabling support for alternative formats like HTML or Markdown. 3. **Unified Section Configuration**: - **Old**: Headers, rows, and footers had separate, inconsistent configuration methods. - **New**: `tw.CellConfig` (`tw/cell.go:CellConfig`) standardizes configuration across headers, rows, and footers, encompassing formatting (`tw.CellFormatting`), padding (`tw.CellPadding`), column widths (`tw.CellWidth`), alignments (`tw.CellAlignment`), and filters (`tw.CellFilter`). 4. **Fluent Configuration Builders**: - **Old**: Configuration was done via individual setters, often requiring multiple method calls. - **New**: `tablewriter.NewConfigBuilder()` (`config.go:NewConfigBuilder`) provides a chained, fluent API for constructing `Config` objects, with nested builders for `Header()`, `Row()`, `Footer()`, `Alignment()`, `Behavior()`, and `ForColumn()` to simplify complex setups. 5. **Explicit Streaming Mode**: - **Old**: No dedicated streaming support; tables were rendered in batch mode. - **New**: Streaming for row-by-row rendering is enabled via `Config.Stream.Enable` or `WithStreaming(tw.StreamConfig{Enable: true})` and managed with `Table.Start()`, `Table.Append()` (or `Table.Header()`, `Table.Footer()`), and `Table.Close()` (`stream.go`). This is ideal for large datasets or continuous output. 6. **Enhanced Error Handling**: - **Old**: Methods like `Render()` did not return errors, making error detection difficult. - **New**: Key methods (`Render()`, `Start()`, `Close()`, `Append()`, `Bulk()`) return errors to promote robust error handling and improve application reliability (`tablewriter.go`, `stream.go`). 7. **Richer Type System & `tw` Package**: - **Old**: Used integer constants (e.g., `ALIGN_CENTER`) and booleans, leading to potential errors. - **New**: The `tw` sub-package introduces type-safe constructs like `tw.State` (`tw/state.go`), `tw.Align` (`tw/types.go`), `tw.Position` (`tw/types.go`), and `tw.CellConfig` (`tw/cell.go`), replacing magic constants and enhancing code clarity. ## Configuration Methods in v1.0.x v1.0.x offers four flexible methods to configure tables, catering to different use cases and complexity levels. Each method can be used independently or combined, providing versatility for both simple and advanced setups. 1. **Using `WithConfig` Option**: - **Description**: Pass a fully populated `tablewriter.Config` struct during `NewTable` initialization using the `WithConfig` option. - **Use Case**: Ideal for predefined, reusable configurations that can be serialized or shared across multiple tables. - **Pros**: Explicit, portable, and suitable for complex setups; allows complete control over all configuration aspects. - **Cons**: Verbose for simple changes, requiring manual struct population. - **Example**: ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { cfg := tablewriter.Config{ Header: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignCenter}, Formatting: tw.CellFormatting{AutoFormat: tw.On}, }, Row: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignLeft}, }, MaxWidth: 80, Behavior: tw.Behavior{TrimSpace: tw.On}, } table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfg)) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Render() } ``` **Output**: ``` ┌───────┬────────┐ │ NAME │ STATUS │ ├───────┼────────┤ │ Node1 │ Ready │ └───────┴────────┘ ``` 2. **Using `Table.Configure` method**: - **Description**: After creating a `Table` instance, use the `Configure` method with a function that modifies the table's `Config` struct. - **Use Case**: Suitable for quick, ad-hoc tweaks post-initialization, especially for simple or dynamic adjustments. - **Pros**: Straightforward for minor changes; no need for additional structs or builders if you already have a `Table` instance. - **Cons**: Less readable for complex configurations compared to a builder; modifications are applied to an existing instance. - **Example**: ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { table := tablewriter.NewTable(os.Stdout) table.Configure(func(cfg *tablewriter.Config) { cfg.Header.Alignment.Global = tw.AlignCenter cfg.Row.Alignment.Global = tw.AlignLeft }) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Render() } ``` **Output**: Same as above. 3. **Standalone `Option` Functions**: - **Description**: Use `WithXxx` functions (e.g., `WithHeaderAlignment`, `WithDebug`) during `NewTable` initialization or via `table.Options()` to apply targeted settings. - **Use Case**: Best for simple, specific configuration changes without needing a full `Config` struct. - **Pros**: Concise, readable, and intuitive for common settings; ideal for minimal setups. - **Cons**: Limited for complex, multi-faceted configurations; requires multiple options for extensive changes. - **Example**: ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAlignment(tw.AlignCenter), tablewriter.WithRowAlignment(tw.AlignLeft), tablewriter.WithDebug(true), ) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Render() } ``` **Output**: Same as above. 4. **Fluent `ConfigBuilder`**: - **Description**: Use `tablewriter.NewConfigBuilder()` to construct a `Config` struct through a chained, fluent API, then apply it with `WithConfig(builder.Build())`. - **Use Case**: Optimal for complex, dynamic, or programmatically generated configurations requiring fine-grained control. - **Pros**: Highly readable, maintainable, and expressive; supports nested builders for sections and columns. - **Cons**: Slightly verbose; requires understanding builder methods and `Build()` call. - **Example**: ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { cnfBuilder := tablewriter.NewConfigBuilder() cnfBuilder.Header().Alignment().WithGlobal(tw.AlignCenter) // Example of configuring a specific column (less emphasis on this for now) // cnfBuilder.ForColumn(0).WithAlignment(tw.AlignLeft).Build() // Call Build() to return to ConfigBuilder table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cnfBuilder.Build())) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Render() } ``` **Output**: Same as above. **Best Practices**: - Use `WithConfig` or `ConfigBuilder` for complex setups or reusable configurations. - Opt for `Option` functions for simple, targeted changes. - Use `Table.Configure` for direct modifications after table creation, but avoid changes during rendering. - Combine methods as needed (e.g., `ConfigBuilder` for initial setup, `Option` functions for overrides). ## Default Parameters in v1.0.x The `defaultConfig()` function (`config.go:defaultConfig`) establishes baseline settings for new tables, ensuring predictable behavior unless overridden. Below is a detailed table of default parameters, organized by configuration section, to help you understand the starting point for table behavior and appearance. | Section | Parameter | Default Value | Description | |---------------|-------------------------------|-----------------------------------|-----------------------------------------------------------------------------| | **Header** | `Alignment.Global` | `tw.AlignCenter` | Centers header text globally unless overridden by `PerColumn`. | | Header | `Alignment.PerColumn` | `[]tw.Align{}` | Empty; falls back to `Global` unless specified. | | Header | `Formatting.AutoFormat` | `tw.On` | Applies title case (e.g., "col_one" → "COL ONE") to header content. | | Header | `Formatting.AutoWrap` | `tw.WrapTruncate` | Truncates long header text with "…" based on width constraints. | | Header | `Formatting.MergeMode` | `tw.MergeNone` | Disables cell merging in headers by default. | | Header | `Padding.Global` | `tw.PaddingDefault` (`" "`) | Adds one space on left and right of header cells. | | Header | `Padding.PerColumn` | `[]tw.Padding{}` | Empty; falls back to `Global` unless specified. | | Header | `ColMaxWidths.Global` | `0` (unlimited) | No maximum content width for header cells unless set. | | Header | `ColMaxWidths.PerColumn` | `tw.NewMapper[int, int]()` | Empty map; no per-column content width limits unless specified. | | Header | `Filter.Global` | `nil` | No global content transformation for header cells. | | Header | `Filter.PerColumn` | `[]func(string) string{}` | No per-column content transformations unless specified. | | **Row** | `Alignment.Global` | `tw.AlignLeft` | Left-aligns row text globally unless overridden by `PerColumn`. | | Row | `Alignment.PerColumn` | `[]tw.Align{}` | Empty; falls back to `Global`. | | Row | `Formatting.AutoFormat` | `tw.Off` | Disables auto-formatting (e.g., title case) for row content. | | Row | `Formatting.AutoWrap` | `tw.WrapNormal` | Wraps long row text naturally at word boundaries based on width constraints.| | Row | `Formatting.MergeMode` | `tw.MergeNone` | Disables cell merging in rows by default. | | Row | `Padding.Global` | `tw.PaddingDefault` (`" "`) | Adds one space on left and right of row cells. | | Row | `Padding.PerColumn` | `[]tw.Padding{}` | Empty; falls back to `Global`. | | Row | `ColMaxWidths.Global` | `0` (unlimited) | No maximum content width for row cells. | | Row | `ColMaxWidths.PerColumn` | `tw.NewMapper[int, int]()` | Empty map; no per-column content width limits. | | Row | `Filter.Global` | `nil` | No global content transformation for row cells. | | Row | `Filter.PerColumn` | `[]func(string) string{}` | No per-column content transformations. | | **Footer** | `Alignment.Global` | `tw.AlignRight` | Right-aligns footer text globally unless overridden by `PerColumn`. | | Footer | `Alignment.PerColumn` | `[]tw.Align{}` | Empty; falls back to `Global`. | | Footer | `Formatting.AutoFormat` | `tw.Off` | Disables auto-formatting for footer content. | | Footer | `Formatting.AutoWrap` | `tw.WrapNormal` | Wraps long footer text naturally. | | Footer | `Formatting.MergeMode` | `tw.MergeNone` | Disables cell merging in footers. | | Footer | `Padding.Global` | `tw.PaddingDefault` (`" "`) | Adds one space on left and right of footer cells. | | Footer | `Padding.PerColumn` | `[]tw.Padding{}` | Empty; falls back to `Global`. | | Footer | `ColMaxWidths.Global` | `0` (unlimited) | No maximum content width for footer cells. | | Footer | `ColMaxWidths.PerColumn` | `tw.NewMapper[int, int]()` | Empty map; no per-column content width limits. | | Footer | `Filter.Global` | `nil` | No global content transformation for footer cells. | | Footer | `Filter.PerColumn` | `[]func(string) string{}` | No per-column content transformations. | | **Global** | `MaxWidth` | `0` (unlimited) | No overall table width limit. | | Global | `Behavior.AutoHide` | `tw.Off` | Displays empty columns (ignored in streaming). | | Global | `Behavior.TrimSpace` | `tw.On` | Trims leading/trailing spaces from cell content. | | Global | `Behavior.Header` | `tw.Control{Hide: tw.Off}` | Shows header if content is provided. | | Global | `Behavior.Footer` | `tw.Control{Hide: tw.Off}` | Shows footer if content is provided. | | Global | `Behavior.Compact` | `tw.Compact{Merge: tw.Off}` | No compact width optimization for merged cells. | | Global | `Debug` | `false` | Disables debug logging. | | Global | `Stream.Enable` | `false` | Disables streaming mode by default. | | Global | `Widths.Global` | `0` (unlimited) | No fixed column width unless specified. | | Global | `Widths.PerColumn` | `tw.NewMapper[int, int]()` | Empty map; no per-column fixed widths unless specified. | **Notes**: - Defaults can be overridden using any configuration method. - `tw.PaddingDefault` is `{Left: " ", Right: " "}` (`tw/preset.go`). - Alignment within `tw.CellFormatting` is deprecated; `tw.CellAlignment` is preferred. `tw.AlignDefault` falls back to `Global` or `tw.AlignLeft` (`tw/types.go`). - Streaming mode uses `Widths` for fixed column sizing (`stream.go`). ## Renderer Types and Customization v1.0.x introduces a flexible rendering system via the `tw.Renderer` interface (`tw/renderer.go:Renderer`), allowing for both default text-based rendering and custom output formats. This decouples rendering logic from table data processing, enabling support for diverse formats like HTML, CSV, or JSON. ### Default Renderer: `renderer.NewBlueprint` - **Description**: `renderer.NewBlueprint()` creates a text-based renderer. Its visual styles are configured using `tw.Rendition`. - **Use Case**: Standard terminal or text output with configurable borders, symbols, and separators. - **Configuration**: Styled via `tw.Rendition`, which controls: - `Borders`: Outer table borders (`tw.Border`) with `tw.State` for each side (`tw/renderer.go`). - `Settings`: Internal lines (`tw.Lines`) and separators (`tw.Separators`) (`tw/renderer.go`). - `Symbols`: Characters for drawing table lines and junctions (`tw.Symbols`) (`tw/symbols.go`). - **Example**: ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" // Import the renderer package "github.com/olekukonko/tablewriter/tw" "os" ) func main() { table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), // Default Blueprint renderer tablewriter.WithRendition(tw.Rendition{ // Apply custom rendition Symbols: tw.NewSymbols(tw.StyleRounded), Borders: tw.Border{Top: tw.On, Bottom: tw.On, Left: tw.On, Right: tw.On}, Settings: tw.Settings{ Separators: tw.Separators{BetweenRows: tw.On}, Lines: tw.Lines{ShowHeaderLine: tw.On}, }, }), ) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Render() } ``` **Output**: ``` ╭───────┬────────╮ │ Name │ Status │ ├───────┼────────┤ │ Node1 │ Ready │ ╰───────┴────────╯ ``` ### Custom Renderer Implementation - **Description**: Implement the `tw.Renderer` interface to create custom output formats (e.g., HTML, CSV). - **Use Case**: Non-text outputs, specialized formatting, or integration with other systems. - **Interface Methods**: - `Start(w io.Writer) error`: Initializes rendering. - `Header(headers [][]string, ctx tw.Formatting)`: Renders header rows. - `Row(row []string, ctx tw.Formatting)`: Renders a data row. - `Footer(footers [][]string, ctx tw.Formatting)`: Renders footer rows. - `Line(ctx tw.Formatting)`: Renders separator lines. - `Close() error`: Finalizes rendering. - `Config() tw.Rendition`: Returns renderer's current rendition configuration. - `Logger(logger *ll.Logger)`: Sets logger for debugging. - **Example (HTML Renderer)**: ```go package main import ( "fmt" "github.com/olekukonko/ll" // For logger type "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "io" "os" ) // BasicHTMLRenderer implements tw.Renderer type BasicHTMLRenderer struct { writer io.Writer config tw.Rendition // Store the rendition logger *ll.Logger err error } func (r *BasicHTMLRenderer) Start(w io.Writer) error { r.writer = w _, r.err = r.writer.Write([]byte("\n")) return r.err } // Header expects [][]string for potentially multi-line headers func (r *BasicHTMLRenderer) Header(headers [][]string, ctx tw.Formatting) { if r.err != nil { return } _, r.err = r.writer.Write([]byte(" \n")) if r.err != nil { return } // Iterate over cells from the context for the current line for _, cellCtx := range ctx.Row.Current { content := fmt.Sprintf(" \n", cellCtx.Data) _, r.err = r.writer.Write([]byte(content)) if r.err != nil { return } } _, r.err = r.writer.Write([]byte(" \n")) } // Row expects []string for a single line row, but uses ctx for actual data func (r *BasicHTMLRenderer) Row(row []string, ctx tw.Formatting) { // row param is less relevant here, ctx.Row.Current is key if r.err != nil { return } _, r.err = r.writer.Write([]byte(" \n")) if r.err != nil { return } for _, cellCtx := range ctx.Row.Current { content := fmt.Sprintf(" \n", cellCtx.Data) _, r.err = r.writer.Write([]byte(content)) if r.err != nil { return } } _, r.err = r.writer.Write([]byte(" \n")) } func (r *BasicHTMLRenderer) Footer(footers [][]string, ctx tw.Formatting) { if r.err != nil { return } // Similar to Header/Row, using ctx.Row.Current for the footer line data // The footers [][]string param might be used if the renderer needs multi-line footer logic r.Row(nil, ctx) // Reusing Row logic, passing nil for the direct row []string as ctx contains the data } func (r *BasicHTMLRenderer) Line(ctx tw.Formatting) { /* No lines in basic HTML */ } func (r *BasicHTMLRenderer) Close() error { if r.err != nil { return r.err } _, r.err = r.writer.Write([]byte("
%s
%s
\n")) return r.err } func (r *BasicHTMLRenderer) Config() tw.Rendition { return r.config } func (r *BasicHTMLRenderer) Logger(logger *ll.Logger) { r.logger = logger } func main() { table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(&BasicHTMLRenderer{ config: tw.Rendition{Symbols: tw.NewSymbols(tw.StyleASCII)}, // Provide a default Rendition })) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Render() } ``` **Output**: ```html
NAME STATUS
Node1 Ready
``` #### Custom Invoice Renderer ```go package main import ( "fmt" "io" "os" "strings" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" ) // InvoiceRenderer implements tw.Renderer for a basic invoice style. type InvoiceRenderer struct { writer io.Writer logger *ll.Logger rendition tw.Rendition } func NewInvoiceRenderer() *InvoiceRenderer { rendition := tw.Rendition{ Borders: tw.BorderNone, Symbols: tw.NewSymbols(tw.StyleNone), Settings: tw.Settings{Separators: tw.SeparatorsNone, Lines: tw.LinesNone}, Streaming: false, } defaultLogger := ll.New("simple-invoice-renderer") return &InvoiceRenderer{logger: defaultLogger, rendition: rendition} } func (r *InvoiceRenderer) Logger(logger *ll.Logger) { if logger != nil { r.logger = logger } } func (r *InvoiceRenderer) Config() tw.Rendition { return r.rendition } func (r *InvoiceRenderer) Start(w io.Writer) error { r.writer = w r.logger.Debug("InvoiceRenderer: Start") return nil } func (r *InvoiceRenderer) formatLine(cells []string, widths tw.Mapper[int, int], cellContexts map[int]tw.CellContext) string { var sb strings.Builder numCols := 0 if widths != nil { // Ensure widths is not nil before calling Len numCols = widths.Len() } for i := 0; i < numCols; i++ { data := "" if i < len(cells) { data = cells[i] } width := 0 if widths != nil { // Check again before Get width = widths.Get(i) } align := tw.AlignDefault if cellContexts != nil { // Check cellContexts if ctx, ok := cellContexts[i]; ok { align = ctx.Align } } paddedCell := tw.Pad(data, " ", width, align) sb.WriteString(paddedCell) if i < numCols-1 { sb.WriteString(" ") // Column separator } } return sb.String() } func (r *InvoiceRenderer) Header(headers [][]string, ctx tw.Formatting) { if r.writer == nil { return } r.logger.Debugf("InvoiceRenderer: Header (lines: %d)", len(headers)) for _, headerLineCells := range headers { lineStr := r.formatLine(headerLineCells, ctx.Row.Widths, ctx.Row.Current) fmt.Fprintln(r.writer, lineStr) } if len(headers) > 0 { totalWidth := 0 if ctx.Row.Widths != nil { ctx.Row.Widths.Each(func(_ int, w int) { totalWidth += w }) if ctx.Row.Widths.Len() > 1 { totalWidth += (ctx.Row.Widths.Len() - 1) * 3 // Separator spaces } } if totalWidth > 0 { fmt.Fprintln(r.writer, strings.Repeat("-", totalWidth)) } } } func (r *InvoiceRenderer) Row(row []string, ctx tw.Formatting) { if r.writer == nil { return } r.logger.Debug("InvoiceRenderer: Row") lineStr := r.formatLine(row, ctx.Row.Widths, ctx.Row.Current) fmt.Fprintln(r.writer, lineStr) } func (r *InvoiceRenderer) Footer(footers [][]string, ctx tw.Formatting) { if r.writer == nil { return } r.logger.Debugf("InvoiceRenderer: Footer (lines: %d)", len(footers)) if len(footers) > 0 { totalWidth := 0 if ctx.Row.Widths != nil { ctx.Row.Widths.Each(func(_ int, w int) { totalWidth += w }) if ctx.Row.Widths.Len() > 1 { totalWidth += (ctx.Row.Widths.Len() - 1) * 3 } } if totalWidth > 0 { fmt.Fprintln(r.writer, strings.Repeat("-", totalWidth)) } } for _, footerLineCells := range footers { lineStr := r.formatLine(footerLineCells, ctx.Row.Widths, ctx.Row.Current) fmt.Fprintln(r.writer, lineStr) } } func (r *InvoiceRenderer) Line(ctx tw.Formatting) { r.logger.Debug("InvoiceRenderer: Line (no-op)") // This simple renderer draws its own lines in Header/Footer. } func (r *InvoiceRenderer) Close() error { r.logger.Debug("InvoiceRenderer: Close") r.writer = nil return nil } func main() { data := [][]string{ {"Product A", "2", "10.00", "20.00"}, {"Super Long Product Name B", "1", "125.50", "125.50"}, {"Item C", "10", "1.99", "19.90"}, } header := []string{"Description", "Qty", "Unit Price", "Total Price"} footer := []string{"", "", "Subtotal:\nTax (10%):\nGRAND TOTAL:", "165.40\n16.54\n181.94"} invoiceRenderer := NewInvoiceRenderer() // Create table and set custom renderer using Options table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(invoiceRenderer), tablewriter.WithAlignment([]tw.Align{ tw.AlignLeft, tw.AlignCenter, tw.AlignRight, tw.AlignRight, }), ) table.Header(header) for _, v := range data { table.Append(v) } // Use the Footer method with strings containing newlines for multi-line cells table.Footer(footer) fmt.Println("Rendering with InvoiceRenderer:") table.Render() // For comparison, render with default Blueprint renderer // Re-create the table or reset it to use a different renderer table2 := tablewriter.NewTable(os.Stdout, tablewriter.WithAlignment([]tw.Align{ tw.AlignLeft, tw.AlignCenter, tw.AlignRight, tw.AlignRight, }), ) table2.Header(header) for _, v := range data { table2.Append(v) } table2.Footer(footer) fmt.Println("\nRendering with Default Blueprint Renderer (for comparison):") table2.Render() } ``` ``` Rendering with InvoiceRenderer: DESCRIPTION QTY UNIT PRICE TOTAL PRICE -------------------------------------------------------------------- Product A 2 10.00 20.00 Super Long Product Name B 1 125.50 125.50 Item C 10 1.99 19.90 -------------------------------------------------------------------- Subtotal: 165.40 -------------------------------------------------------------------- Tax (10%): 16.54 -------------------------------------------------------------------- GRAND TOTAL: 181.94 ``` ``` Rendering with Default Blueprint Renderer (for comparison): ┌───────────────────────────┬─────┬──────────────┬─────────────┐ │ DESCRIPTION │ QTY │ UNIT PRICE │ TOTAL PRICE │ ├───────────────────────────┼─────┼──────────────┼─────────────┤ │ Product A │ 2 │ 10.00 │ 20.00 │ │ Super Long Product Name B │ 1 │ 125.50 │ 125.50 │ │ Item C │ 10 │ 1.99 │ 19.90 │ ├───────────────────────────┼─────┼──────────────┼─────────────┤ │ │ │ Subtotal: │ 165.40 │ │ │ │ Tax (10%): │ 16.54 │ │ │ │ GRAND TOTAL: │ 181.94 │ └───────────────────────────┴─────┴──────────────┴─────────────┘ ``` **Notes**: - The `renderer.NewBlueprint()` is sufficient for most text-based use cases. - Custom renderers require implementing all interface methods to handle table structure correctly. `tw.Formatting` (which includes `tw.RowContext`) provides cell content and metadata. ## Function Mapping Table (v0.0.5 → v1.0.x) The following table maps v0.0.5 methods to their v1.0.x equivalents, ensuring a quick reference for migration. All deprecated methods are retained for compatibility but should be replaced with new APIs. | v0.0.5 Method | v1.0.x Equivalent(s) | Notes | |-----------------------------------|--------------------------------------------------------------------------------------|----------------------------------------------------------------------| | `NewWriter(w)` | `NewTable(w, opts...)` | `NewWriter` deprecated; wraps `NewTable` (`tablewriter.go`). | | `SetHeader([]string)` | `Header(...any)` | Variadic or slice; supports any type (`tablewriter.go`). | | `Append([]string)` | `Append(...any)` | Variadic, slice, or struct (`tablewriter.go`). | | `AppendBulk([][]string)` | `Bulk([]any)` | Slice of rows; supports any type (`tablewriter.go`). | | `SetFooter([]string)` | `Footer(...any)` | Variadic or slice; supports any type (`tablewriter.go`). | | `Render()` | `Render()` (returns `error`) | Batch mode; streaming uses `Start/Close` (`tablewriter.go`). | | `SetBorder(bool)` | `WithRendition(tw.Rendition{Borders: ...})` or `WithBorders(tw.Border)` (deprecated) | Use `tw.Border` (`deprecated.go`, `tw/renderer.go`). | | `SetRowLine(bool)` | `WithRendition(tw.Rendition{Settings: {Separators: {BetweenRows: tw.On}}})` | `tw.Separators` (`tw/renderer.go`). | | `SetHeaderLine(bool)` | `WithRendition(tw.Rendition{Settings: {Lines: {ShowHeaderLine: tw.On}}})` | `tw.Lines` (`tw/renderer.go`). | | `SetColumnSeparator(string)` | `WithRendition(tw.Rendition{Symbols: ...})` or `WithSymbols(tw.Symbols)` (deprecated)| `tw.NewSymbols` or custom `tw.Symbols` (`tw/symbols.go`). | | `SetCenterSeparator(string)` | `WithRendition(tw.Rendition{Symbols: ...})` or `WithSymbols(tw.Symbols)` (deprecated)| `tw.Symbols` (`tw/symbols.go`). | | `SetRowSeparator(string)` | `WithRendition(tw.Rendition{Symbols: ...})` or `WithSymbols(tw.Symbols)` (deprecated)| `tw.Symbols` (`tw/symbols.go`). | | `SetAlignment(int)` | `WithRowAlignment(tw.Align)` or `Config.Row.Alignment.Global` | `tw.Align` type (`config.go`). | | `SetHeaderAlignment(int)` | `WithHeaderAlignment(tw.Align)` or `Config.Header.Alignment.Global` | `tw.Align` type (`config.go`). | | `SetAutoFormatHeaders(bool)` | `WithHeaderAutoFormat(tw.State)` or `Config.Header.Formatting.AutoFormat` | `tw.State` (`config.go`). | | `SetAutoWrapText(bool)` | `WithRowAutoWrap(int)` or `Config.Row.Formatting.AutoWrap` (uses `tw.Wrap...` const) | `tw.WrapNormal`, `tw.WrapTruncate`, etc. (`config.go`). | | `SetAutoMergeCells(bool)` | `WithRowMergeMode(int)` or `Config.Row.Formatting.MergeMode` (uses `tw.Merge...` const) | Supports `Vertical`, `Hierarchical` (`config.go`). | | `SetColMinWidth(col, w)` | `WithColumnWidths(tw.NewMapper[int,int]().Set(col, w))` or `Config.Widths.PerColumn` | `Config.Widths` for fixed widths (`config.go`). | | `SetTablePadding(string)` | Use `Config.
.Padding.Global` with `tw.Padding` | No direct equivalent; manage via cell padding (`tw/cell.go`). | | `SetDebug(bool)` | `WithDebug(bool)` or `Config.Debug` | Logs via `table.Debug()` (`config.go`). | | `Clear()` | `Reset()` | Clears data and state (`tablewriter.go`). | | `SetNoWhiteSpace(bool)` | `WithTrimSpace(tw.Off)` (if `true`) or `WithPadding(tw.PaddingNone)` | Manage via `Config.Behavior.TrimSpace` or padding (`config.go`). | | `SetColumnColor(Colors)` | Embed ANSI codes in cell data, use `tw.Formatter`, or `Config.
.Filter` | Colors via data or filters (`tw/cell.go`). | | `SetHeaderColor(Colors)` | Embed ANSI codes in cell data, use `tw.Formatter`, or `Config.Header.Filter` | Colors via data or filters (`tw/cell.go`). | | `SetFooterColor(Colors)` | Embed ANSI codes in cell data, use `tw.Formatter`, or `Config.Footer.Filter` | Colors via data or filters (`tw/cell.go`). | **Notes**: - Deprecated methods are in `deprecated.go` but should be replaced. - `tw` package types (e.g., `tw.Align`, `tw.State`) are required for new APIs. - Examples for each mapping are provided in the migration steps below. ## Detailed Migration Steps: Initialization, Data Input, Rendering This section provides step-by-step guidance for migrating core table operations, with code examples and explanations to ensure clarity. Each step maps v0.0.5 methods to v1.0.x equivalents, highlighting changes, best practices, and potential pitfalls. ### 1. Table Initialization Initialization has shifted from `NewWriter` to `NewTable`, which supports flexible configuration via `Option` functions, `Config` structs, or `ConfigBuilder`. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) // ... use table _ = table // Avoid "declared but not used" } ``` **New (v1.0.x):** ```go package main import ( "fmt" // Added for FormattableEntry example "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" // Import renderer "github.com/olekukonko/tablewriter/tw" "os" "strings" // Added for Formatter example ) func main() { // Minimal Setup (Default Configuration) tableMinimal := tablewriter.NewTable(os.Stdout) _ = tableMinimal // Avoid "declared but not used" // Using Option Functions for Targeted Configuration tableWithOptions := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAlignment(tw.AlignCenter), // Center header text tablewriter.WithRowAlignment(tw.AlignLeft), // Left-align row text tablewriter.WithDebug(true), // Enable debug logging ) _ = tableWithOptions // Avoid "declared but not used" // Using a Full Config Struct for Comprehensive Control cfg := tablewriter.Config{ Header: tw.CellConfig{ Alignment: tw.CellAlignment{ Global: tw.AlignCenter, PerColumn: []tw.Align{tw.AlignLeft, tw.AlignRight}, // Column-specific alignment }, Formatting: tw.CellFormatting{AutoFormat: tw.On}, }, Row: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignLeft}, Formatting: tw.CellFormatting{AutoWrap: tw.WrapNormal}, }, Footer: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignRight}, }, MaxWidth: 80, // Constrain total table width Behavior: tw.Behavior{ AutoHide: tw.Off, // Show empty columns TrimSpace: tw.On, // Trim cell spaces }, Widths: tw.CellWidth{ Global: 20, // Default fixed column width PerColumn: tw.NewMapper[int, int]().Set(0, 15), // Column 0 fixed at 15 }, } tableWithConfig := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfg)) _ = tableWithConfig // Avoid "declared but not used" // Using ConfigBuilder for Fluent, Complex Configuration builder := tablewriter.NewConfigBuilder(). WithMaxWidth(80). WithAutoHide(tw.Off). WithTrimSpace(tw.On). WithDebug(true). // Enable debug logging Header(). Alignment(). WithGlobal(tw.AlignCenter). Build(). // Returns *ConfigBuilder Header(). Formatting(). WithAutoFormat(tw.On). WithAutoWrap(tw.WrapTruncate). // Test truncation WithMergeMode(tw.MergeNone). // Explicit merge mode Build(). // Returns *HeaderConfigBuilder Padding(). WithGlobal(tw.Padding{Left: "[", Right: "]", Overwrite: true}). Build(). // Returns *HeaderConfigBuilder Build(). // Returns *ConfigBuilder Row(). Formatting(). WithAutoFormat(tw.On). // Uppercase rows Build(). // Returns *RowConfigBuilder Build(). // Returns *ConfigBuilder Row(). Alignment(). WithGlobal(tw.AlignLeft). Build(). // Returns *ConfigBuilder Build() // Finalize Config tableWithFluent := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(builder)) _ = tableWithFluent // Avoid "declared but not used" } ``` **Key Changes**: - **Deprecation**: `NewWriter` is deprecated but retained for compatibility, internally calling `NewTable` (`tablewriter.go:NewWriter`). Transition to `NewTable` for new code. - **Flexibility**: `NewTable` accepts an `io.Writer` and variadic `Option` functions (`tablewriter.go:NewTable`), enabling configuration via: - `WithConfig(Config)`: Applies a complete `Config` struct (`config.go:WithConfig`). - `WithRenderer(tw.Renderer)`: Sets a custom renderer, defaulting to `renderer.NewBlueprint()` (`tablewriter.go:WithRenderer`). - `WithRendition(tw.Rendition)`: Configures visual styles for the renderer (`tablewriter.go:WithRendition`). - `WithStreaming(tw.StreamConfig)`: Enables streaming mode (`tablewriter.go:WithStreaming`). - Other `WithXxx` functions for specific settings (e.g., `WithHeaderAlignment`, `WithDebug`). - **ConfigBuilder**: Provides a fluent API for complex setups; `Build()` finalizes the `Config` (`config.go:ConfigBuilder`). - **Method Chaining in Builder**: Remember to call `Build()` on nested builders to return to the parent builder (e.g., `builder.Header().Alignment().WithGlobal(...).Build().Formatting()...`). **Migration Tips**: - Replace `NewWriter` with `NewTable` and specify configurations explicitly. - Use `ConfigBuilder` for complex setups or when readability is paramount. - Apply `Option` functions for quick, targeted changes. - Ensure `tw` package is imported for types like `tw.Align` and `tw.CellConfig`. - Verify renderer settings if custom styling is needed, as `renderer.NewBlueprint()` is the default. **Potential Pitfalls**: - **Unintended Defaults**: Without explicit configuration, `defaultConfig()` applies (see Default Parameters), which may differ from v0.0.5 behavior (e.g., `Header.Formatting.AutoFormat = tw.On`). - **Renderer Absence**: If no renderer is set, `NewTable` defaults to `renderer.NewBlueprint()`; explicitly set for custom formats. - **ConfigBuilder Errors**: Always call `Build()` at the end of a builder chain and on nested builders; omitting it can lead to incomplete configurations or runtime errors. - **Concurrent Modification**: Avoid modifying `Table.config` (if using the `Configure` method or direct access) in concurrent scenarios or during rendering to prevent race conditions. ### 2. Data Input Data input methods in v1.0.x are more flexible, accepting `any` type for headers, rows, and footers, with robust conversion logic to handle strings, structs, and custom types. #### 2.1. Setting Headers Headers define the table’s column labels and are typically the first data added. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Name", "Sign", "Rating"}) // ... } ``` **New (v1.0.x):** ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" // For tw.Formatter "os" "strings" ) // Struct with Formatter type HeaderData struct { // Renamed to avoid conflict Label string } func (h HeaderData) Format() string { return strings.ToUpper(h.Label) } // Implements tw.Formatter func main() { table := tablewriter.NewTable(os.Stdout) // Variadic Arguments (Preferred for Simplicity) table.Header("Name", "Sign", "Rating") // Slice of Strings // table.Header([]string{"Name", "Sign", "Rating"}) // Example, comment out if using variadic // Slice of Any Type (Flexible) // table.Header([]any{"Name", "Sign", 123}) // Numbers converted to strings // Using Formatter // table.Header(HeaderData{"name"}, HeaderData{"sign"}, HeaderData{"rating"}) // Outputs "NAME", "SIGN", "RATING" table.Append("Example", "Row", "Data") // Add some data to render table.Render() } ``` **Key Changes**: - **Method**: `SetHeader([]string)` replaced by `Header(...any)` (`tablewriter.go:Header`). - **Flexibility**: Accepts variadic arguments or a single slice; supports any type, not just strings. - **Conversion**: Elements are processed by `processVariadic` (`zoo.go:processVariadic`) and converted to strings via `convertCellsToStrings` (`zoo.go:convertCellsToStrings`), supporting: - Basic types (e.g., `string`, `int`, `float64`). - `fmt.Stringer` implementations. - `tw.Formatter` implementations (custom string conversion). - Structs (exported fields as cells or single cell if `Stringer`/`Formatter`). - **Streaming**: In streaming mode, `Header()` renders immediately via `streamRenderHeader` (`stream.go:streamRenderHeader`). - **Formatting**: Headers are formatted per `Config.Header` settings (e.g., `AutoFormat`, `Alignment`) during rendering (`zoo.go:prepareContent`). **Migration Tips**: - Replace `SetHeader` with `Header`, using variadic arguments for simplicity. - Use slices for dynamic header lists or when passing from a variable. - Implement `tw.Formatter` for custom header formatting (e.g., uppercase). - Ensure header count matches expected columns to avoid width mismatches. - In streaming mode, call `Header()` before rows, as it fixes column widths (`stream.go`). **Potential Pitfalls**: - **Type Mismatch**: Non-string types are converted to strings; ensure desired formatting (e.g., use `tw.Formatter` for custom types). - **Streaming Widths**: Headers influence column widths in streaming; set `Config.Widths` explicitly if specific widths are needed (`stream.go:streamCalculateWidths`). - **Empty Headers**: Missing headers may cause rendering issues; provide placeholders (e.g., `""`) if needed. - **AutoFormat**: Default `Header.Formatting.AutoFormat = tw.On` applies title case; disable with `WithHeaderAutoFormat(tw.Off)` if unwanted (`config.go`). #### 2.2. Appending Rows Rows represent the table’s data entries, and v1.0.x enhances flexibility with support for varied input types. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"ColA", "ColB", "ColC"}) // Add header for context table.Append([]string{"A", "The Good", "500"}) table.Append([]string{"B", "The Very Bad", "288"}) data := [][]string{ {"C", "The Ugly", "120"}, {"D", "The Gopher", "800"}, } table.AppendBulk(data) table.Render() } ``` **New (v1.0.x):** ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" // For tw.Formatter "log" "os" ) // Struct for examples type Entry struct { ID string Label string Score int } // Struct with Formatter type FormattableEntry struct { ID string Label string Score int } func (e FormattableEntry) Format() string { return fmt.Sprintf("%s (%d)", e.Label, e.Score) } // Implements tw.Formatter func main() { table := tablewriter.NewTable(os.Stdout) table.Header("ID", "Description", "Value") // Header for context // Variadic Arguments (Single Row) table.Append("A", "The Good", 500) // Numbers converted to strings // Slice of Any Type (Single Row) table.Append([]any{"B", "The Very Bad", "288"}) // Struct with Fields table.Append(Entry{ID: "C", Label: "The Ugly", Score: 120}) // Fields as cells // Struct with Formatter (will produce a single cell for this row based on Format()) // To make it fit 3 columns, the formatter would need to produce a string that looks like 3 cells, or the table config would need to adapt. // For this example, let's assume it's meant to be one wide cell, or adjust the header. // For now, let's simplify and append it to a table with one header. tableOneCol := tablewriter.NewTable(os.Stdout) tableOneCol.Header("Formatted Entry") tableOneCol.Append(FormattableEntry{ID: "D", Label: "The Gopher", Score: 800}) // Single cell "The Gopher (800)" tableOneCol.Render() fmt.Println("---") // Re-initialize main table for Bulk example table = tablewriter.NewTable(os.Stdout) table.Header("ID", "Description", "Value") // Multiple Rows with Bulk data := []any{ []any{"E", "The Fast", 300}, Entry{ID: "F", Label: "The Swift", Score: 400}, // Struct instance // FormattableEntry{ID: "G", Label: "The Bold", Score: 500}, // Would also be one cell } if err := table.Bulk(data); err != nil { log.Fatalf("Bulk append failed: %v", err) } table.Render() } ``` **Key Changes**: - **Method**: `Append([]string)` replaced by `Append(...any)`; `AppendBulk([][]string)` replaced by `Bulk([]any)` (`tablewriter.go`). - **Input Flexibility**: `Append` accepts: - Multiple arguments as cells of a single row. - A single slice (e.g., `[]string`, `[]any`) as cells of a single row. - A single struct, processed by `convertItemToCells` (`zoo.go`): - Uses `tw.Formatter` or `fmt.Stringer` for single-cell output. - Extracts exported fields as multiple cells otherwise. - **Bulk Input**: `Bulk` accepts a slice where each element is a row (e.g., `[][]any`, `[]Entry`), processed by `appendSingle` (`zoo.go:appendSingle`). - **Streaming**: Rows render immediately in streaming mode via `streamAppendRow` (`stream.go:streamAppendRow`). - **Error Handling**: `Append` and `Bulk` return errors for invalid conversions or streaming issues (`tablewriter.go`). **Migration Tips**: - Replace `Append([]string)` with `Append(...any)` for variadic input. - Use `Bulk` for multiple rows, ensuring each element is a valid row representation. - Implement `tw.Formatter` for custom struct formatting to control cell output. - Match row cell count to headers to avoid alignment issues. - In streaming mode, append rows after `Start()` and before `Close()` (`stream.go`). **Potential Pitfalls**: - **Cell Count Mismatch**: Uneven row lengths may cause rendering errors; pad with empty strings (e.g., `""`) if needed. - **Struct Conversion**: Unexported fields are ignored; ensure fields are public or use `Formatter`/`Stringer` (`zoo.go`). - **Streaming Order**: Rows must be appended after headers in streaming to maintain width consistency (`stream.go`). - **Error Ignored**: Always check `Bulk` errors, as invalid data can cause failures. #### 2.3. Setting Footers Footers provide summary or closing data for the table, often aligned differently from rows. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"ColA", "ColB", "ColC", "ColD"}) // Add header for context table.SetFooter([]string{"", "", "Total", "1408"}) table.Render() } ``` **New (v1.0.x):** ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" // For tw.Formatter "os" ) // Using Formatter type FooterSummary struct { // Renamed to avoid conflict Label string Value float64 } func (s FooterSummary) Format() string { return fmt.Sprintf("%s: %.2f", s.Label, s.Value) } // Implements tw.Formatter func main() { table := tablewriter.NewTable(os.Stdout) table.Header("ColA", "ColB", "ColC", "ColD") // Header for context // Variadic Arguments table.Footer("", "", "Total", 1408) // Slice of Any Type // table.Footer([]any{"", "", "Total", 1408.50}) table.Render() // Render this table fmt.Println("--- Another Table with Formatter Footer ---") table2 := tablewriter.NewTable(os.Stdout) table2.Header("Summary Info") // Single column header // Using Formatter for a single cell footer table2.Footer(FooterSummary{Label: "Grand Total", Value: 1408.50}) // Single cell: "Grand Total: 1408.50" table2.Render() } ``` **Key Changes**: - **Method**: `SetFooter([]string)` replaced by `Footer(...any)` (`tablewriter.go:Footer`). - **Input Flexibility**: Like `Header`, accepts variadic arguments, slices, or structs with `Formatter`/`Stringer` support (`zoo.go:processVariadic`). - **Streaming**: Footers are buffered via `streamStoreFooter` and rendered during `Close()` (`stream.go:streamStoreFooter`, `stream.go:streamRenderFooter`). - **Formatting**: Controlled by `Config.Footer` settings (e.g., `Alignment`, `AutoWrap`) (`zoo.go:prepareTableSection`). **Migration Tips**: - Replace `SetFooter` with `Footer`, preferring variadic input for simplicity. - Use `tw.Formatter` for custom footer formatting (e.g., formatted numbers). - Ensure footer cell count matches headers/rows to maintain alignment. - In streaming mode, call `Footer()` before `Close()` to include it in rendering. **Potential Pitfalls**: - **Alignment Differences**: Default `Footer.Alignment.Global = tw.AlignRight` differs from rows (`tw.AlignLeft`); adjust if needed (`config.go`). - **Streaming Timing**: Footers not rendered until `Close()`; ensure `Close()` is called (`stream.go`). - **Empty Footers**: Missing footers may affect table appearance; use placeholders if needed. ### 3. Rendering the Table Rendering has been overhauled to support both batch and streaming modes, with mandatory error handling for robustness. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Data"}) table.Append([]string{"Example"}) table.Render() } ``` **New (v1.0.x) - Batch Mode:** ```go package main import ( "github.com/olekukonko/tablewriter" "log" "os" ) func main() { table := tablewriter.NewTable(os.Stdout) table.Header("Data") table.Append("Example") if err := table.Render(); err != nil { log.Fatalf("Table rendering failed: %v", err) } } ``` **New (v1.0.x) - Streaming Mode:** ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "log" "os" ) func main() { tableStream := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(tablewriter.Config{ Stream: tw.StreamConfig{Enable: true}, Widths: tw.CellWidth{ Global: 12, // Fixed column width for streaming PerColumn: tw.NewMapper[int, int]().Set(0, 15), // Column 0 at 15 }, }), ) // Alternative: Using WithStreaming Option // tableStream := tablewriter.NewTable(os.Stdout, // tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), // tablewriter.WithColumnMax(12), // Sets Config.Widths.Global // ) if err := tableStream.Start(); err != nil { log.Fatalf("Failed to start table stream: %v", err) } tableStream.Header("Column 1", "Column 2") for i := 0; i < 3; i++ { if err := tableStream.Append(fmt.Sprintf("Data %d-1", i), fmt.Sprintf("Data %d-2", i)); err != nil { log.Printf("Failed to append row %d: %v", i, err) // Log and continue or handle differently } } tableStream.Footer("End", "Summary") if err := tableStream.Close(); err != nil { log.Fatalf("Failed to close table stream: %v", err) } } ``` **Key Changes**: - **Batch Mode**: - `Render()` processes all data (headers, rows, footers) in one go (`tablewriter.go:render`). - Calculates column widths dynamically based on content, `Config.Widths`, `ColMaxWidths`, and `MaxWidth` (`zoo.go:calculateAndNormalizeWidths`). - Invokes renderer methods: `Start()`, `Header()`, `Row()`, `Footer()`, `Line()`, `Close()` (`tw/renderer.go`). - Returns errors for invalid configurations or I/O issues. - **Streaming Mode**: - Enabled via `Config.Stream.Enable` or `WithStreaming(tw.StreamConfig{Enable: true})` (`tablewriter.go:WithStreaming`). - `Start()` initializes the stream, fixing column widths based on `Config.Widths` or first data (header/row) (`stream.go:streamCalculateWidths`). - `Header()`, `Append()` render immediately (`stream.go:streamRenderHeader`, `stream.go:streamAppendRow`). - `Footer()` buffers data, rendered by `Close()` (`stream.go:streamStoreFooter`, `stream.go:streamRenderFooter`). - `Close()` finalizes rendering with footer and borders (`stream.go:Close`). - All methods return errors for robust handling. - **Error Handling**: Mandatory error checks replace silent failures, improving reliability. **Migration Tips**: - Replace `Render()` calls with error-checked versions in batch mode. - For streaming, adopt `Start()`, `Append()`, `Close()` workflow, ensuring `Start()` precedes data input. - Set `Config.Widths` for consistent column widths in streaming mode (`config.go`). - Use `WithRendition` to customize visual output, as renderer settings are decoupled (`tablewriter.go`). - Test rendering with small datasets to verify configuration before scaling. **Potential Pitfalls**: - **Missing Error Checks**: Failing to check errors can miss rendering failures; always use `if err != nil`. - **Streaming Widths**: Widths are fixed after `Start()`; inconsistent data may cause truncation (`stream.go`). - **Renderer Misconfiguration**: Ensure `tw.Rendition` matches desired output style (`tw/renderer.go`). - **Incomplete Streaming**: Forgetting `Close()` in streaming mode omits footer and final borders (`stream.go`). - **Batch vs. Streaming**: Using `Render()` in streaming mode causes errors; use `Start()`/`Close()` instead (`tablewriter.go`). ## Styling and Appearance Configuration Styling in v1.0.x is split between `tablewriter.Config` for data processing (e.g., alignment, padding, wrapping) and `tw.Rendition` for visual rendering (e.g., borders, symbols, lines). This section details how to migrate v0.0.5 styling methods to v1.0.x, providing examples, best practices, and migration tips to maintain or enhance table appearance. ### 4.1. Table Styles Table styles define the visual structure through border and separator characters. In v0.0.5, styles were set via individual separator methods, whereas v1.0.x uses `tw.Rendition.Symbols` for a cohesive approach, offering predefined styles and custom symbol sets. **Available Table Styles**: The `tw.Symbols` interface (`tw/symbols.go`) supports a variety of predefined styles, each tailored to specific use cases, as well as custom configurations for unique requirements. | Style Name | Use Case | Symbols Example | Recommended Context | |----------------|-----------------------------------|-------------------------------------|-----------------------------------------| | `StyleASCII` | Simple, terminal-friendly | `+`, `-`, `|` | Basic CLI output, minimal dependencies; ensures compatibility across all terminals, including older or non-Unicode systems. | | `StyleLight` | Clean, lightweight borders | `┌`, `└`, `─`, `│` | Modern terminals, clean and professional aesthetics; suitable for most CLI applications requiring a modern look. | | `StyleHeavy` | Thick, prominent borders | `┏`, `┗`, `━`, `┃` | High-visibility reports, dashboards, or logs; emphasizes table structure for critical data presentation. | | `StyleDouble` | Double-line borders | `╔`, `╚`, `═`, `║` | Formal documents, structured data exports; visually distinct for presentations or printed outputs. | | `StyleRounded` | Rounded corners, aesthetic | `╭`, `╰`, `─`, `│` | User-friendly CLI applications, reports; enhances visual appeal with smooth, rounded edges. | | `StyleMarkdown`| Markdown-compatible | `|`, `-`, `:` | Documentation, GitHub READMEs, or Markdown-based platforms; ensures proper rendering in Markdown viewers. | *Note: `StyleBold` was mentioned but not defined in the symbols code, `StyleHeavy` is similar.* **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) // table.SetCenterSeparator("*") // Example, actual v0.0.5 method might vary // table.SetColumnSeparator("!") // table.SetRowSeparator("=") // table.SetBorder(true) table.SetHeader([]string{"Header1", "Header2"}) table.Append([]string{"Cell1", "Cell2"}) table.Render() } ``` **New (v1.0.x):** ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { // Using a Predefined Style (Rounded Corners for Aesthetic Output) tableStyled := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), // Ensure renderer is set tablewriter.WithRendition(tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleRounded), // Predefined rounded style Borders: tw.Border{Top: tw.On, Bottom: tw.On, Left: tw.On, Right: tw.On}, Settings: tw.Settings{ Separators: tw.Separators{BetweenRows: tw.On}, // Lines between rows }, }), ) tableStyled.Header("Name", "Status") tableStyled.Append("Node1", "Ready") tableStyled.Render() // Using Fully Custom Symbols (Mimicking v0.0.5 Custom Separators) mySymbols := tw.NewSymbolCustom("my-style"). // Fluent builder for custom symbols WithCenter("*"). // Junction of lines WithColumn("!"). // Vertical separator WithRow("="). // Horizontal separator WithTopLeft("/"). WithTopMid("-"). WithTopRight("\\"). WithMidLeft("["). WithMidRight("]"). // Corrected from duplicate WithMidRight("+") // WithMidMid was not a method in SymbolCustom WithBottomLeft("\\"). WithBottomMid("_"). WithBottomRight("/"). WithHeaderLeft("("). WithHeaderMid("-"). WithHeaderRight(")") // Header-specific tableCustomSymbols := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), tablewriter.WithRendition(tw.Rendition{ Symbols: mySymbols, Borders: tw.Border{Top: tw.On, Bottom: tw.On, Left: tw.On, Right: tw.On}, }), ) tableCustomSymbols.Header("Name", "Status") tableCustomSymbols.Append("Node1", "Ready") tableCustomSymbols.Render() } ``` **Output (Styled, Rounded):** ``` ╭───────┬────────╮ │ NAME │ STATUS │ ├───────┼────────┤ │ Node1 │ Ready │ ╰───────┴────────╯ ``` **Output (Custom Symbols):** ``` /=======-========\ ! NAME ! STATUS ! [=======*========] ! Node1 ! Ready ! \=======_========/ ``` **Key Changes**: - **Deprecated Methods**: `SetCenterSeparator`, `SetColumnSeparator`, `SetRowSeparator`, and `SetBorder` are deprecated and moved to `deprecated.go`. These are replaced by `tw.Rendition.Symbols` and `tw.Rendition.Borders` for a unified styling approach (`tablewriter.go`). - **Symbol System**: The `tw.Symbols` interface defines all drawing characters used for table lines, junctions, and corners (`tw/symbols.go:Symbols`). - `tw.NewSymbols(tw.BorderStyle)` provides predefined styles (e.g., `tw.StyleASCII`, `tw.StyleMarkdown`, `tw.StyleRounded`) for common use cases. (Note: `tw.Style` should be `tw.BorderStyle`) - `tw.NewSymbolCustom(name string)` enables fully custom symbol sets via a fluent builder, allowing precise control over each character (e.g., `WithCenter`, `WithTopLeft`). - **Renderer Dependency**: Styling requires a renderer, set via `WithRenderer`; the default is `renderer.NewBlueprint()` if not specified (`tablewriter.go:WithRenderer`). - **Border Control**: `tw.Rendition.Borders` uses `tw.State` (`tw.On`, `tw.Off`) to enable or disable borders on each side (Top, Bottom, Left, Right) (`tw/renderer.go:Border`). - **Extensibility**: Custom styles can be combined with custom renderers for non-text outputs, enhancing flexibility (`tw/renderer.go`). **Migration Tips**: - Replace individual separator setters (`SetCenterSeparator`, etc.) with `tw.NewSymbols` for predefined styles or `tw.NewSymbolCustom` for custom designs to maintain or enhance v0.0.5 styling. - Use `WithRendition` to apply `tw.Rendition` settings, ensuring a renderer is explicitly set to avoid default behavior (`tablewriter.go`). - Test table styles in your target environment (e.g., terminal, Markdown viewer, or log file) to ensure compatibility with fonts and display capabilities. - For Markdown-based outputs (e.g., GitHub READMEs), use `tw.StyleMarkdown` to ensure proper rendering in Markdown parsers (`tw/symbols.go`). - Combine `tw.Rendition.Symbols` with `tw.Rendition.Borders` and `tw.Rendition.Settings` to replicate or improve v0.0.5’s border and line configurations (`tw/renderer.go`). - Document custom symbol sets in code comments to aid maintenance, as they can be complex (`tw/symbols.go`). **Potential Pitfalls**: - **Missing Renderer**: If `WithRenderer` is omitted, `NewTable` defaults to `renderer.NewBlueprint` with minimal styling, which may not match v0.0.5’s `SetBorder(true)` behavior; always specify for custom styles (`tablewriter.go`). - **Incomplete Custom Symbols**: When using `tw.NewSymbolCustom`, failing to set all required symbols (e.g., `TopLeft`, `Center`, `HeaderLeft`) can cause rendering errors or inconsistent visuals; ensure all necessary characters are defined (`tw/symbols.go`). - **Terminal Compatibility Issues**: Advanced styles like `StyleDouble` or `StyleHeavy` may not render correctly in older terminals or non-Unicode environments; use `StyleASCII` for maximum compatibility across platforms (`tw/symbols.go`). - **Border and Symbol Mismatch**: Inconsistent `tw.Rendition.Borders` and `tw.Symbols` settings (e.g., enabling borders but using minimal symbols) can lead to visual artifacts; test with small tables to verify alignment (`tw/renderer.go`). - **Markdown Rendering**: Non-Markdown styles (e.g., `StyleRounded`) may not render correctly in Markdown viewers; use `StyleMarkdown` for documentation or web-based outputs (`tw/symbols.go`). ### 4.2. Borders and Separator Lines Borders and internal lines define the table’s structural appearance, controlling the visibility of outer edges and internal divisions. In v0.0.5, these were set via specific methods, while v1.0.x uses `tw.Rendition` fields for a more integrated approach. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) // table.SetBorder(false) // Disable all outer borders // table.SetRowLine(true) // Enable lines between data rows // table.SetHeaderLine(true) // Enable line below header table.SetHeader([]string{"Header1", "Header2"}) table.Append([]string{"Cell1", "Cell2"}) table.Render() } ``` **New (v1.0.x):** ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { // Standard Bordered Table with Internal Lines table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), tablewriter.WithRendition(tw.Rendition{ Borders: tw.Border{ // Outer table borders Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On, }, Settings: tw.Settings{ Lines: tw.Lines{ // Major internal separator lines ShowHeaderLine: tw.On, // Line after header ShowFooterLine: tw.On, // Line before footer (if footer exists) }, Separators: tw.Separators{ // General row and column separators BetweenRows: tw.On, // Horizontal lines between data rows BetweenColumns: tw.On, // Vertical lines between columns }, }, }), ) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Render() // Borderless Table (kubectl-style, No Lines or Separators) // Configure the table with a borderless, kubectl-style layout config := tablewriter.NewConfigBuilder(). Header(). Padding(). WithGlobal(tw.PaddingNone). // No header padding Build(). Alignment(). WithGlobal(tw.AlignLeft). // Left-align header Build(). Row(). Padding(). WithGlobal(tw.PaddingNone). // No row padding Build(). Alignment(). WithGlobal(tw.AlignLeft). // Left-align rows Build(). Footer(). Padding(). WithGlobal(tw.PaddingNone). // No footer padding Build(). Alignment(). WithGlobal(tw.AlignLeft). // Left-align footer (if used) Build(). WithDebug(true). // Enable debug logging Build() // Create borderless table tableBorderless := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), // Assumes valid renderer tablewriter.WithRendition(tw.Rendition{ Borders: tw.BorderNone, // No borders Symbols: tw.NewSymbols(tw.StyleNone), // No border symbols Settings: tw.Settings{ Lines: tw.LinesNone, // No header/footer lines Separators: tw.SeparatorsNone, // No row/column separators }, }), tablewriter.WithConfig(config), ) // Set headers and data tableBorderless.Header("Name", "Status") tableBorderless.Append("Node1", "Ready") tableBorderless.Render() } ``` **Output (Standard Bordered):** ``` ┌───────┬────────┐ │ Name │ Status │ ├───────┼────────┤ │ Node1 │ Ready │ └───────┴────────┘ ``` **Output (Borderless):** ``` NAME STATUS Node1 Ready ``` **Key Changes**: - **Deprecated Methods**: `SetBorder`, `SetRowLine`, and `SetHeaderLine` are deprecated and moved to `deprecated.go`. These are replaced by fields in `tw.Rendition` (`tw/renderer.go`): - `Borders`: Controls outer table borders (`tw.Border`) with `tw.State` (`tw.On`, `tw.Off`) for each side (Top, Bottom, Left, Right). - `Settings.Lines`: Manages major internal lines (`ShowHeaderLine` for header, `ShowFooterLine` for footer) (`tw.Lines`). - `Settings.Separators`: Controls general separators between rows (`BetweenRows`) and columns (`BetweenColumns`) (`tw.Separators`). - **Presets for Simplicity**: `tw.BorderNone`, `tw.LinesNone`, and `tw.SeparatorsNone` provide quick configurations for minimal or borderless tables (`tw/preset.go`). - **Renderer Integration**: Border and line settings are applied via `WithRendition`, requiring a renderer to be set (`tablewriter.go:WithRendition`). - **Granular Control**: Each border side and line type can be independently configured, offering greater flexibility than v0.0.5’s boolean toggles. - **Dependency on Symbols**: The appearance of borders and lines depends on `tw.Rendition.Symbols`; ensure compatible symbol sets (`tw/symbols.go`). **Migration Tips**: - Replace `SetBorder(false)` with `tw.Rendition.Borders = tw.BorderNone` to disable all outer borders (`tw/preset.go`). - Use `tw.Rendition.Settings.Separators.BetweenRows = tw.On` to replicate `SetRowLine(true)`, ensuring row separators are visible (`tw/renderer.go`). - Set `tw.Rendition.Settings.Lines.ShowHeaderLine = tw.On` to mimic `SetHeaderLine(true)` for a line below the header (`tw/renderer.go`). - For kubectl-style borderless tables, combine `tw.BorderNone`, `tw.LinesNone`, `tw.SeparatorsNone`, and `WithPadding(tw.PaddingNone)` (applied via `ConfigBuilder` or `WithConfig`) to eliminate all lines and spacing (`tw/preset.go`, `config.go`). - Test border and line configurations with small tables to verify visual consistency, especially when combining with custom `tw.Symbols`. - Use `WithDebug(true)` to log rendering details if borders or lines appear incorrectly (`config.go`). **Potential Pitfalls**: - **Separator Absence**: If `tw.Rendition.Settings.Separators.BetweenColumns` is `tw.Off` and borders are disabled, columns may lack visual separation; use `tw.CellPadding` or ensure content spacing (`tw/cell.go`). - **Line and Border Conflicts**: Mismatched settings (e.g., enabling `ShowHeaderLine` but disabling `Borders.Top`) can cause uneven rendering; align `Borders`, `Lines`, and `Separators` settings (`tw/renderer.go`). - **Renderer Dependency**: Border settings require a renderer; omitting `WithRenderer` defaults to `renderer.NewBlueprint` with minimal styling, which may not match v0.0.5 expectations (`tablewriter.go`). - **Streaming Limitations**: In streaming mode, separator rendering is fixed after `Start()`; ensure `tw.Rendition` is set correctly before rendering begins (`stream.go`). - **Symbol Mismatch**: Using minimal `tw.Symbols` (e.g., `StyleASCII`) with complex `Borders` settings may lead to visual artifacts; test with matching symbol sets (`tw/symbols.go`). ### 4.3. Alignment Alignment controls the positioning of text within table cells, now configurable per section (Header, Row, Footer) with both global and per-column options for greater precision. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" // Assuming ALIGN_CENTER etc. were constants here "os" ) const ( // Mocking v0.0.5 constants for example completeness ALIGN_CENTER = 1 // Example values ALIGN_LEFT = 2 ) func main() { table := tablewriter.NewWriter(os.Stdout) // table.SetAlignment(ALIGN_CENTER) // Applied to data rows // table.SetHeaderAlignment(ALIGN_LEFT) // Applied to header // No specific footer alignment setter table.SetHeader([]string{"Header1", "Header2"}) table.Append([]string{"Cell1", "Cell2"}) table.Render() } ``` **New (v1.0.x):** ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { cfg := tablewriter.Config{ Header: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignLeft}, }, Row: tw.CellConfig{ Alignment: tw.CellAlignment{ Global: tw.AlignCenter, PerColumn: []tw.Align{tw.AlignLeft, tw.AlignRight}, // Col 0 left, Col 1 right }, }, Footer: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignRight}, }, } table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfg)) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Footer("", "Active") // Ensure footer has content to show alignment table.Render() } ``` **Output:** ``` ┌───────┬────────┐ │ NAME │ STATUS │ ├───────┼────────┤ │ Node1 │ Ready │ ├───────┼────────┤ │ │ Active │ └───────┴────────┘ ``` **Key Changes**: - **Deprecated Methods**: `SetAlignment` and `SetHeaderAlignment` are replaced by `WithRowAlignment`, `WithHeaderAlignment`, `WithFooterAlignment`, or direct `Config` settings (`config.go`). These old methods are retained in `deprecated.go` for compatibility but should be migrated. - **Alignment Structure**: Alignment is managed within `tw.CellConfig.Alignment` (`tw/cell.go:CellAlignment`), which includes: - `Global`: A single `tw.Align` value applied to all cells in the section (`tw.AlignLeft`, `tw.AlignCenter`, `tw.AlignRight`, `tw.AlignNone`). - `PerColumn`: A slice of `tw.Align` values for column-specific alignment, overriding `Global` for specified columns. - **Footer Alignment**: v1.0.x introduces explicit footer alignment via `WithFooterAlignment` or `Config.Footer.Alignment`, addressing v0.0.5’s lack of footer-specific control (`config.go`). - **Type Safety**: `tw.Align` string constants replace v0.0.5’s integer constants (e.g., `ALIGN_CENTER`), improving clarity and reducing errors (`tw/types.go`). - **Builder Support**: `ConfigBuilder` provides `Alignment()` methods for each section. `ForColumn(idx).WithAlignment()` applies alignment to a specific column across all sections (`config.go:ConfigBuilder`, `config.go:ColumnConfigBuilder`). - **Deprecated Fields**: `tw.CellConfig.ColumnAligns` (slice) and `tw.CellFormatting.Alignment` (single value) are supported for backward compatibility but should be migrated to `tw.CellAlignment.Global` and `tw.CellAlignment.PerColumn` (`tw/cell.go`). **Migration Tips**: - Replace `SetAlignment(ALIGN_X)` with `WithRowAlignment(tw.AlignX)` or `Config.Row.Alignment.Global = tw.AlignX` to set row alignment (`config.go`). - Use `WithHeaderAlignment(tw.AlignX)` for headers and `WithFooterAlignment(tw.AlignX)` for footers to maintain or adjust v0.0.5 behavior (`config.go`). - Specify per-column alignments with `ConfigBuilder.Row().Alignment().WithPerColumn([]tw.Align{...})` or by setting `Config.Row.Alignment.PerColumn` for fine-grained control (`config.go`). - Use `ConfigBuilder.ForColumn(idx).WithAlignment(tw.AlignX)` to apply consistent alignment to a specific column across all sections (Header, Row, Footer) (`config.go`). - Verify alignment settings against defaults (`Header: tw.AlignCenter`, `Row: tw.AlignLeft`, `Footer: tw.AlignRight`) to ensure expected output (`config.go:defaultConfig`). - Test alignment with varied cell content lengths to confirm readability, especially when combined with wrapping or padding settings (`zoo.go:prepareContent`). **Potential Pitfalls**: - **Alignment Precedence**: `PerColumn` settings override `Global` within a section; ensure column-specific alignments are intentional (`tw/cell.go:CellAlignment`). - **Deprecated Fields**: Relying on `ColumnAligns` or `tw.CellFormatting.Alignment` is temporary; migrate to `tw.CellAlignment` to avoid future issues (`tw/cell.go`). - **Cell Count Mismatch**: Rows or footers with fewer cells than headers can cause alignment errors; pad with empty strings (`""`) to match column count (`zoo.go`). - **Streaming Width Impact**: In streaming mode, alignment depends on fixed column widths set by `Config.Widths`; narrow widths may truncate content, misaligning text (`stream.go:streamCalculateWidths`). - **Default Misalignment**: The default `Footer.Alignment.Global = tw.AlignRight` differs from rows (`tw.AlignLeft`); explicitly set `WithFooterAlignment` if consistency is needed (`config.go`). ### 4.4. Auto-Formatting (Header Title Case) Auto-formatting applies transformations like title case to cell content, primarily for headers to enhance readability, but it can be enabled for rows or footers in v1.0.x. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) // table.SetAutoFormatHeaders(true) // Default: true; applies title case to headers table.SetHeader([]string{"col_one", "status_report"}) table.Append([]string{"Node1", "Ready"}) table.Render() } ``` **New (v1.0.x):** ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { // Using Direct Config Struct to turn OFF default AutoFormat for Header cfg := tablewriter.Config{ Header: tw.CellConfig{Formatting: tw.CellFormatting{AutoFormat: tw.Off}}, // Turn OFF title case for headers Row: tw.CellConfig{Formatting: tw.CellFormatting{AutoFormat: tw.Off}}, // No formatting for rows (default) Footer: tw.CellConfig{Formatting: tw.CellFormatting{AutoFormat: tw.Off}}, // No formatting for footers (default) } tableNoAutoFormat := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfg)) tableNoAutoFormat.Header("col_one", "status_report") // Stays as "col_one", "status_report" tableNoAutoFormat.Append("Node1", "Ready") tableNoAutoFormat.Render() // Using Option Function for Headers (default is ON, this makes it explicit) tableWithAutoFormat := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.On), // Explicitly enable title case (default behavior) ) tableWithAutoFormat.Header("col_one", "status_report") // Becomes "COL ONE", "STATUS REPORT" tableWithAutoFormat.Append("Node1", "Ready") tableWithAutoFormat.Render() } ``` **Output:** ``` ┌─────────┬───────────────┐ │ col_one │ status_report │ ├─────────┼───────────────┤ │ Node1 │ Ready │ └─────────┴───────────────┘ ┌─────────┬───────────────┐ │ COL ONE │ STATUS REPORT │ ├─────────┼───────────────┤ │ Node1 │ Ready │ └─────────┴───────────────┘ ``` **Key Changes**: - **Method**: `SetAutoFormatHeaders` is replaced by `Config.Header.Formatting.AutoFormat`, or equivalent builder methods (`config.go`). - **Extended Scope**: Auto-formatting can now be applied to rows and footers via `Config.Row.Formatting.AutoFormat` and `Config.Footer.Formatting.AutoFormat`, unlike v0.0.5’s header-only support (`tw/cell.go`). - **Type Safety**: `tw.State` (`tw.On`, `tw.Off`, `tw.Unknown`) controls formatting state, replacing boolean flags (`tw/state.go`). - **Behavior**: When `tw.On`, the `tw.Title` function converts text to uppercase and replaces underscores and some periods with spaces (e.g., "col_one" → "COL ONE") (`tw/fn.go:Title`, `zoo.go:prepareContent`). - **Defaults**: `Header.Formatting.AutoFormat = tw.On`, `Row.Formatting.AutoFormat = tw.Off`, `Footer.Formatting.AutoFormat = tw.Off` (`config.go:defaultConfig`). **Migration Tips**: - To maintain v0.0.5’s default header title case behavior, no explicit action is needed as `WithHeaderAutoFormat(tw.On)` is the default. If you were using `SetAutoFormatHeaders(false)`, you'd use `WithHeaderAutoFormat(tw.Off)`. - Explicitly set `WithRowAutoFormat(tw.Off)` or `WithFooterAutoFormat(tw.Off)` if you want to ensure rows and footers remain unformatted, as v1.0.x allows enabling formatting for these sections (`config.go`). - Use `ConfigBuilder.Header().Formatting().WithAutoFormat(tw.State)` for precise control over formatting per section (`config.go`). - Test header output with underscores or periods (e.g., "my_column") to verify title case transformation meets expectations. - For custom formatting beyond title case (e.g., custom capitalization), use `tw.CellFilter` instead of `AutoFormat` (`tw/cell.go`). **Potential Pitfalls**: - **Default Header Formatting**: `Header.Formatting.AutoFormat = tw.On` by default may unexpectedly alter header text (e.g., "col_one" → "COL ONE"); disable with `WithHeaderAutoFormat(tw.Off)` if raw headers are preferred (`config.go`). - **Row/Footer Formatting**: Enabling `AutoFormat` for rows or footers (not default) applies title case, which may not suit data content; verify with `tw.Off` unless intended (`tw/cell.go`). - **Filter Conflicts**: Combining `AutoFormat` with `tw.CellFilter` can lead to overlapping transformations; prioritize filters for complex formatting (`zoo.go:prepareContent`). - **Performance Overhead**: Auto-formatting large datasets may add minor processing time; disable for performance-critical applications (`zoo.go`). ### 4.5. Text Wrapping Text wrapping determines how long cell content is handled within width constraints, offering more options in v1.0.x compared to v0.0.5’s binary toggle. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) // table.SetAutoWrapText(true) // Enable normal word wrapping // table.SetAutoWrapText(false) // Disable wrapping table.SetHeader([]string{"Long Header Text Example", "Status"}) table.Append([]string{"This is some very long cell content that might wrap", "Ready"}) table.Render() } ``` **New (v1.0.x):** ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" // For tw.WrapNormal etc. "os" ) func main() { // Using Option Functions for Quick Wrapping Settings on a specific section (e.g., Row) // To actually see wrapping, a MaxWidth for the table or columns is needed. table := tablewriter.NewTable(os.Stdout, tablewriter.WithRowAutoWrap(tw.WrapNormal), // Set row wrapping tablewriter.WithHeaderAutoWrap(tw.WrapTruncate), // Header truncates (default) tablewriter.WithMaxWidth(30), // Force wrapping by setting table max width ) table.Header("Long Header Text", "Status") table.Append("This is a very long cell content", "Ready") table.Footer("Summary", "Active") table.Render() // For more fine-grained control (e.g., different wrapping for header, row, footer): cfgBuilder := tablewriter.NewConfigBuilder() cfgBuilder.Header().Formatting().WithAutoWrap(tw.WrapTruncate) // Header: Truncate cfgBuilder.Row().Formatting().WithAutoWrap(tw.WrapNormal) // Row: Normal wrap cfgBuilder.Footer().Formatting().WithAutoWrap(tw.WrapBreak) // Footer: Break words cfgBuilder.WithMaxWidth(40) // Overall table width constraint tableFullCfg := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfgBuilder.Build())) tableFullCfg.Header("Another Very Long Header Example That Will Be Truncated", "Info") tableFullCfg.Append("This is an example of row content that should wrap normally based on available space.", "Detail") tableFullCfg.Footer("FinalSummaryInformationThatMightBreakAcrossLines", "End") tableFullCfg.Render() } ``` **Output (tableOpt):** ``` ┌──────────────┬────────┐ │ LONG HEADER… │ STATUS │ ├──────────────┼────────┤ │ This is a │ Ready │ │ very long │ │ │ cell content │ │ ├──────────────┼────────┤ │ Summary │ Active │ └──────────────┴────────┘ ``` *(Second table output will vary based on content and width)* **Key Changes**: - **Method**: `SetAutoWrapText` is replaced by `Config.
.Formatting.AutoWrap` or specific `With
AutoWrap` options (`config.go`). - **Wrapping Modes**: `int` constants for `AutoWrap` (e.g., `tw.WrapNone`, `tw.WrapNormal`, `tw.WrapTruncate`, `tw.WrapBreak`) replace v0.0.5’s binary toggle (`tw/tw.go`). - **Section-Specific Control**: Wrapping is configurable per section (Header, Row, Footer), unlike v0.0.5’s global setting (`tw/cell.go`). - **Defaults**: `Header: tw.WrapTruncate`, `Row: tw.WrapNormal`, `Footer: tw.WrapNormal` (`config.go:defaultConfig`). - **Width Dependency**: Wrapping behavior relies on width constraints set by `Config.Widths` (fixed column widths), `Config.
.ColMaxWidths` (max content width), or `Config.MaxWidth` (total table width) (`zoo.go:calculateContentMaxWidth`, `zoo.go:prepareContent`). **Migration Tips**: - Replace `SetAutoWrapText(true)` with `WithRowAutoWrap(tw.WrapNormal)` to maintain v0.0.5’s default wrapping for rows (`config.go`). - Use `WithHeaderAutoWrap(tw.WrapTruncate)` to align with v1.0.x’s default header behavior, ensuring long headers are truncated (`config.go`). - Set `Config.Widths` or `Config.MaxWidth` explicitly to enforce wrapping, as unconstrained widths may prevent wrapping (`config.go`). - Use `ConfigBuilder.
().Formatting().WithAutoWrap(int_wrap_mode)` for precise control over wrapping per section (`config.go`). - Test wrapping with varied content lengths (e.g., short and long text) to ensure readability and proper width allocation. - Consider `tw.WrapNormal` for data rows to preserve content integrity, reserving `tw.WrapTruncate` for headers or footers (`tw/tw.go`). **Potential Pitfalls**: - **Missing Width Constraints**: Without `Config.Widths`, `ColMaxWidths`, or `MaxWidth`, wrapping may not occur, leading to overflow; always define width limits for wrapping (`zoo.go`). - **Streaming Width Impact**: In streaming mode, wrapping depends on fixed widths set at `Start()`; narrow widths may truncate content excessively (`stream.go:streamCalculateWidths`). - **Truncation Data Loss**: `tw.WrapTruncate` may obscure critical data in rows; use `tw.WrapNormal` or wider columns to retain content (`tw/tw.go`). - **Performance Overhead**: Wrapping large datasets with `tw.WrapNormal` or `tw.WrapBreak` can add processing time; optimize widths for performance-critical applications (`zoo.go:prepareContent`). - **Inconsistent Section Wrapping**: Default wrapping differs (`Header: tw.WrapTruncate`, `Row/Footer: tw.WrapNormal`); align settings if uniform behavior is needed (`config.go`). ### 4.6. Padding Padding adds spacing within cells, enhancing readability and affecting cell width calculations. v1.0.x introduces granular, per-side padding, replacing v0.0.5’s single inter-column padding control. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) // table.SetTablePadding("\t") // Set inter-column space character when borders are off // Default: single space within cells table.SetHeader([]string{"Header1"}) table.Append([]string{"Cell1"}) table.Render() } ``` **New (v1.0.x):** ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { // Using Direct Config Struct cfg := tablewriter.Config{ Header: tw.CellConfig{ Padding: tw.CellPadding{ Global: tw.Padding{Left: "[", Right: "]", Top: "-", Bottom: "-"}, // Padding for all header cells PerColumn: []tw.Padding{ // Specific padding for header column 0 {Left: ">>", Right: "<<", Top: "=", Bottom: "="}, // Overrides Global for column 0 }, }, }, Row: tw.CellConfig{ Padding: tw.CellPadding{ Global: tw.PaddingDefault, // One space left/right for all row cells }, }, Footer: tw.CellConfig{ Padding: tw.CellPadding{ Global: tw.Padding{Top: "~", Bottom: "~"}, // Top/bottom padding for all footer cells }, }, } table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfg)) table.Header("Name", "Status") // Two columns table.Append("Node1", "Ready") table.Footer("End", "Active") table.Render() } ``` **Output:** ``` ┌────────┬────────┐ │========│[------]│ │>>NAME<<│[STATUS]│ │========│[------]│ ├────────┼────────┤ │ Node1 │ Ready │ │~~~~~~~~│~~~~~~~~│ │End │Active │ │~~~~~~~~│~~~~~~~~│ └────────┴────────┘ ``` **Key Changes**: - **No Direct Equivalent for `SetTablePadding`**: `SetTablePadding` controlled inter-column spacing when borders were off in v0.0.5; v1.0.x has no direct equivalent for *inter-column* spacing separate from cell padding. Inter-column visual separation is now primarily handled by `tw.Rendition.Settings.Separators.BetweenColumns` and the chosen `tw.Symbols`. - **Granular Cell Padding**: `tw.CellPadding` (`tw/cell.go:CellPadding`) supports: - `Global`: A `tw.Padding` struct with `Left`, `Right`, `Top`, `Bottom` strings and an `Overwrite` flag (`tw/types.go:Padding`). This padding is *inside* the cell. - `PerColumn`: A slice of `tw.Padding` for column-specific padding, overriding `Global` for specified columns. - **Per-Side Control**: `Top` and `Bottom` padding add extra lines *within* cells, unlike v0.0.5’s left/right-only spacing (`zoo.go:prepareContent`). - **Defaults**: `tw.PaddingDefault` is `{Left: " ", Right: " "}` for all sections (applied inside cells); `Top` and `Bottom` are empty by default (`tw/preset.go`). - **Width Impact**: Cell padding contributes to column widths, calculated in `Config.Widths` (`zoo.go:calculateAndNormalizeWidths`). - **Presets**: `tw.PaddingNone` (`{Left: "", Right: "", Top: "", Bottom: "", Overwrite: true}`) removes padding for tight layouts (`tw/preset.go`). **Migration Tips**: - To achieve spacing similar to `SetTablePadding("\t")` when borders are off, you would set cell padding: `WithPadding(tw.Padding{Left: "\t", Right: "\t"})`. If you truly mean space *between* columns and not *inside* cells, ensure `tw.Rendition.Settings.Separators.BetweenColumns` is `tw.On` and customize `tw.Symbols.Column()` if needed. - Use `tw.PaddingNone` (e.g., via `ConfigBuilder.
().Padding().WithGlobal(tw.PaddingNone)`) for no cell padding. - Set `Top` and `Bottom` padding for vertical spacing *within* cells, enhancing readability for multi-line content (`tw/types.go`). - Use `ConfigBuilder.
().Padding().WithPerColumn` for column-specific padding to differentiate sections or columns (`config.go`). - Test padding with varied content and widths to ensure proper alignment and spacing, especially with wrapping enabled (`zoo.go`). - Combine padding with `Config.Widths` or `ColMaxWidths` to control total cell size (`config.go`). **Potential Pitfalls**: - **Inter-Column Spacing vs. Cell Padding**: Be clear whether you want space *between* columns (separators) or *inside* cells (padding). `SetTablePadding` was ambiguous; v1.0.x distinguishes these. - **Width Inflation**: Cell padding increases column widths, potentially exceeding `Config.MaxWidth` or causing truncation in streaming; adjust `Config.Widths` accordingly (`zoo.go`). - **Top/Bottom Padding**: Non-empty `Top` or `Bottom` padding adds vertical lines *within* cells, increasing cell height; use sparingly for dense tables (`zoo.go:prepareContent`). - **Streaming Constraints**: Padding is fixed in streaming mode after `Start()`; ensure `Config.Widths` accommodates padding (`stream.go`). - **Default Padding**: `tw.PaddingDefault` adds spaces *inside* cells; set `tw.PaddingNone` for no internal cell padding (`tw/preset.go`). ### 4.7. Column Widths (Fixed Widths and Max Content Widths) Column width control in v1.0.x is more sophisticated, offering fixed widths, maximum content widths, and overall table width constraints, replacing v0.0.5’s limited `SetColMinWidth`. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) // table.SetColMinWidth(0, 10) // Set minimum width for column 0 table.SetHeader([]string{"Header1", "Header2"}) table.Append([]string{"Cell1-Content", "Cell2-Content"}) table.Render() } ``` **New (v1.0.x):** ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { // Direct Config for Width Control cfg := tablewriter.Config{ Widths: tw.CellWidth{ // Fixed total column widths (content + padding) Global: 20, // Default fixed width for all columns PerColumn: tw.NewMapper[int, int]().Set(0, 15), // Column 0 fixed at 15 }, Header: tw.CellConfig{ ColMaxWidths: tw.CellWidth{ // Max content width (excluding padding) for header cells Global: 15, // Max content width for all header cells PerColumn: tw.NewMapper[int, int]().Set(0, 10), // Header Col 0 max content at 10 }, // Default header padding is " " on left/right, so content 10 + padding 2 = 12. // If Widths.PerColumn[0] is 15, there's space. }, Row: tw.CellConfig{ ColMaxWidths: tw.CellWidth{Global: 18}, // Max content width for row cells (18 + default padding 2 = 20) }, MaxWidth: 80, // Constrain total table width (optional, columns might shrink) } tableWithCfg := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfg)) tableWithCfg.Header("Very Long Header Name", "Status Information") tableWithCfg.Append("Some long content for the first column", "Ready") tableWithCfg.Render() // Option Functions for Width Settings tableWithOpts := tablewriter.NewTable(os.Stdout, tablewriter.WithColumnMax(20), // Sets Config.Widths.Global (fixed total col width) tablewriter.WithColumnWidths(tw.NewMapper[int, int]().Set(0, 15)), // Sets Config.Widths.PerColumn for col 0 tablewriter.WithMaxWidth(80), // Sets Config.MaxWidth // For max content width per section, you'd use WithHeaderConfig, WithRowConfig, etc. // e.g., tablewriter.WithRowMaxWidth(18) // Sets Config.Row.ColMaxWidths.Global ) tableWithOpts.Header("Long Header", "Status") tableWithOpts.Append("Long Content", "Ready") tableWithOpts.Render() } ``` **Output (tableWithCfg - illustrative, exact wrapping depends on content and full config):** ``` ┌───────────┬──────────────────┐ │ VERY LONG │ STATUS INFORMAT… │ │ HEADER NA…│ │ ├───────────┼──────────────────┤ │ Some long │ Ready │ │ content f…│ │ └───────────┴──────────────────┘ ``` **Key Changes**: - **Enhanced Width System**: v1.0.x introduces three levels of width control, replacing `SetColMinWidth` (`config.go`): - **Config.Widths**: Sets fixed total widths (content + padding) for columns, applied globally or per-column (`tw.CellWidth`). - `Global`: Default fixed width for all columns. - `PerColumn`: `tw.Mapper[int, int]` for specific column widths. - **Config.
.ColMaxWidths**: Sets maximum content widths (excluding padding) for a section (Header, Row, Footer) (`tw.CellWidth`). - `Global`: Max content width for all cells in the section. - `PerColumn`: `tw.Mapper[int, int]` for specific columns in the section. - **Config.MaxWidth**: Constrains the total table width, shrinking columns proportionally if needed (`config.go`). - **Streaming Support**: In streaming mode, `Config.Widths` fixes column widths at `Start()`; `ColMaxWidths` is used only for wrapping/truncation (`stream.go:streamCalculateWidths`). - **Calculation Logic**: Widths are computed by `calculateAndNormalizeWidths` in batch mode and `streamCalculateWidths` in streaming mode, considering content, padding, and constraints (`zoo.go`, `stream.go`). - **Deprecated Approach**: `SetColMinWidth` is replaced by `Config.Widths.PerColumn` or `Config.
.ColMaxWidths.PerColumn` for more precise control (`deprecated.go`). **Migration Tips**: - Replace `SetColMinWidth(col, w)` with `WithColumnWidths(tw.NewMapper[int, int]().Set(col, w))` for fixed column widths or `Config.
.ColMaxWidths.PerColumn` for content width limits (`config.go`). - Use `Config.Widths.Global` or `WithColumnMax(w)` to set a default fixed width for all columns, ensuring consistency (`tablewriter.go`). - Apply `Config.MaxWidth` to constrain total table width, especially for wide datasets (`config.go`). - Use `ConfigBuilder.ForColumn(idx).WithMaxWidth(w)` to set per-column content width limits across sections (`config.go`). *(Note: This sets it for Header, Row, and Footer)* - In streaming mode, set `Config.Widths` before `Start()` to fix widths, avoiding content-based sizing (`stream.go`). - Test width settings with varied content to ensure wrapping and truncation behave as expected (`zoo.go`). **Potential Pitfalls**: - **Width Precedence**: `Config.Widths.PerColumn` overrides `Widths.Global`; `ColMaxWidths` applies *within* those fixed total widths for content wrapping/truncation (`zoo.go`). - **Streaming Width Fixing**: Widths are locked after `Start()` in streaming; inconsistent data may cause truncation (`stream.go`). - **Padding Impact**: Padding adds to total width when considering `Config.Widths`; account for `tw.CellPadding` when setting fixed column widths (`zoo.go`). - **MaxWidth Shrinkage**: `Config.MaxWidth` may shrink columns unevenly; test with `MaxWidth` to avoid cramped layouts (`zoo.go`). - **No Width Constraints**: Without `Widths` or `MaxWidth`, columns size to content, potentially causing overflow; define limits (`zoo.go`). ### 4.8. Colors Colors in v0.0.5 were applied via specific color-setting methods, while v1.0.x embeds ANSI escape codes in cell content or uses data-driven formatting for greater flexibility. **Old (v0.0.5):** ```go package main // Assuming tablewriter.Colors and color constants existed in v0.0.5 // This is a mock representation as the actual v0.0.5 definitions are not provided. // import "github.com/olekukonko/tablewriter" // import "os" // type Colors []interface{} // Mock // const ( // Bold = 1; FgGreenColor = 2; FgRedColor = 3 // Mock constants // ) func main() { // table := tablewriter.NewWriter(os.Stdout) // table.SetColumnColor( // tablewriter.Colors{tablewriter.Bold, tablewriter.FgGreenColor}, // Column 0 // tablewriter.Colors{tablewriter.FgRedColor}, // Column 1 // ) // table.SetHeader([]string{"Name", "Status"}) // table.Append([]string{"Node1", "Ready"}) // table.Render() } ``` **New (v1.0.x):** ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" // For tw.Formatter "os" ) // Direct ANSI Code Embedding const ( Reset = "\033[0m" Bold = "\033[1m" FgGreen = "\033[32m" FgRed = "\033[31m" ) // Using tw.Formatter for Custom Types type Status string func (s Status) Format() string { // Implements tw.Formatter color := FgGreen if s == "Error" || s == "Inactive" { color = FgRed } return color + string(s) + Reset } func main() { // Example 1: Direct ANSI embedding tableDirectANSI := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off), // Keep header text as is for coloring ) tableDirectANSI.Header(Bold+FgGreen+"Name"+Reset, Bold+FgRed+"Status"+Reset) tableDirectANSI.Append([]any{"Node1", FgGreen + "Ready" + Reset}) tableDirectANSI.Append([]any{"Node2", FgRed + "Error" + Reset}) tableDirectANSI.Render() fmt.Println("\n--- Table with Formatter for Colors ---") // Example 2: Using tw.Formatter tableFormatter := tablewriter.NewTable(os.Stdout) tableFormatter.Header("Name", "Status") // AutoFormat will apply to "NAME", "STATUS" tableFormatter.Append([]any{"Alice", Status("Active")}) tableFormatter.Append([]any{"Bob", Status("Inactive")}) tableFormatter.Render() fmt.Println("\n--- Table with CellFilter for Colors ---") // Example 3: Using CellFilter tableWithFilters := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig( tablewriter.NewConfigBuilder(). Row(). Filter(). WithPerColumn([]func(string) string{ nil, // No filter for Item column func(s string) string { // Status column: apply color if s == "Ready" || s == "Active" { return FgGreen + s + Reset } return FgRed + s + Reset }, }). Build(). // Return to RowConfigBuilder Build(). // Return to ConfigBuilder Build(), // Finalize Config ), ) tableWithFilters.Header("Item", "Availability") tableWithFilters.Append("ItemA", "Ready") tableWithFilters.Append("ItemB", "Unavailable") tableWithFilters.Render() } ``` **Output (Text Approximation, Colors Not Shown):** ``` ┌──────┬────────┐ │ Name │ Status │ ├──────┼────────┤ │Node1 │ Ready │ │Node2 │ Error │ └──────┴────────┘ --- Table with Formatter for Colors --- ┌───────┬──────────┐ │ NAME │ STATUS │ ├───────┼──────────┤ │ Alice │ Active │ │ Bob │ Inactive │ └───────┴──────────┘ --- Table with CellFilter for Colors --- ┌───────┬────────────┐ │ ITEM │ AVAILABILI │ │ │ TY │ ├───────┼────────────┤ │ ItemA │ Ready │ │ ItemB │ Unavailabl │ │ │ e │ └───────┴────────────┘ ``` **Key Changes**: - **Removed Color Methods**: `SetColumnColor`, `SetHeaderColor`, and `SetFooterColor` are removed; colors are now applied by embedding ANSI escape codes in cell content or via data-driven mechanisms (`tablewriter.go`). - **Flexible Coloring Options**: - **Direct ANSI Codes**: Embed codes (e.g., `\033[32m` for green) in strings for manual control (`zoo.go:convertCellsToStrings`). - **tw.Formatter**: Implement `Format() string` on custom types to control cell output, including colors (`tw/types.go:Formatter`). - **tw.CellFilter**: Use `Config.
.Filter.Global` or `PerColumn` to apply transformations like coloring dynamically (`tw/cell.go:CellFilter`). - **Width Handling**: `twdw.Width()` correctly calculates display width of ANSI-coded strings, ignoring escape sequences (`tw/fn.go:DisplayWidth`). - **No Built-In Color Presets**: Unlike v0.0.5’s potential `tablewriter.Colors`, v1.0.x requires manual ANSI code management or external libraries for color constants. **Migration Tips**: - Replace `SetColumnColor` with direct ANSI code embedding for simple cases (e.g., `FgGreen+"text"+Reset`) (`zoo.go`). - Implement `tw.Formatter` on custom types for reusable, semantic color logic (e.g., `Status` type above) (`tw/types.go`). - Use `ConfigBuilder.
().Filter().WithPerColumn` to apply color filters to specific columns, mimicking v0.0.5’s per-column coloring (`config.go`). - Define ANSI constants in your codebase or use a library (e.g., `github.com/fatih/color`) to simplify color management. - Test colored output in your target terminal to ensure ANSI codes render correctly. - Combine filters with `AutoFormat` or wrapping for consistent styling (`zoo.go:prepareContent`). **Potential Pitfalls**: - **Terminal Support**: Some terminals may not support ANSI codes, causing artifacts; test in your environment or provide a non-colored fallback. - **Filter Overlap**: Combining `tw.CellFilter` with `AutoFormat` or other transformations can lead to unexpected results; prioritize filters for coloring (`zoo.go`). - **Width Miscalculation**: Incorrect ANSI code handling (e.g., missing `Reset`) can skew width calculations; use `twdw.Width` (`tw/fn.go`). - **Streaming Consistency**: In streaming mode, ensure color codes are applied consistently across rows to avoid visual discrepancies (`stream.go`). - **Performance**: Applying filters to large datasets may add overhead; optimize filter logic for efficiency (`zoo.go`). ## Advanced Features in v1.0.x v1.0.x introduces several advanced features that enhance table functionality beyond v0.0.5’s capabilities. This section covers cell merging, captions, filters, stringers, and performance optimizations, providing migration guidance and examples for leveraging these features. ### 5. Cell Merging Cell merging combines adjacent cells with identical content, improving readability for grouped data. v1.0.x expands merging options beyond v0.0.5’s horizontal merging. **Old (v0.0.5):** ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewWriter(os.Stdout) // table.SetAutoMergeCells(true) // Enable horizontal merging table.SetHeader([]string{"Category", "Item", "Notes"}) table.Append([]string{"Fruit", "Apple", "Red"}) table.Append([]string{"Fruit", "Apple", "Green"}) // "Apple" might merge if SetAutoMergeCells was on table.Render() } ``` **New (v1.0.x):** ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { // Horizontal Merging (Similar to v0.0.5) tableH := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(tablewriter.Config{Row: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}}}), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: tw.NewSymbols(tw.StyleASCII)})), // Specify renderer for symbols ) tableH.Header("Category", "Item", "Item", "Notes") // Note: Two "Item" headers for demo tableH.Append("Fruit", "Apple", "Apple", "Red") // "Apple" cells merge tableH.Render() // Vertical Merging tableV := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(tablewriter.Config{Row: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeVertical}}}), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: tw.NewSymbols(tw.StyleASCII)})), ) tableV.Header("User", "Permission") tableV.Append("Alice", "Read") tableV.Append("Alice", "Write") // "Alice" cells merge vertically tableV.Append("Bob", "Read") tableV.Render() // Hierarchical Merging tableHier := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(tablewriter.Config{Row: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical}}}), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: tw.NewSymbols(tw.StyleASCII)})), ) tableHier.Header("Group", "SubGroup", "Item") tableHier.Append("Tech", "CPU", "i7") tableHier.Append("Tech", "CPU", "i9") // "Tech" and "CPU" merge tableHier.Append("Tech", "RAM", "16GB") // "Tech" merges, "RAM" is new tableHier.Append("Office", "CPU", "i5") // "Office" is new tableHier.Render() } ``` **Output (Horizontal):** ``` +----------+-------+-------+-------+ | CATEGORY | ITEM | ITEM | NOTES | +----------+-------+-------+-------+ | Fruit | Apple | Red | +----------+---------------+-------+ ``` **Output (Vertical):** ``` +-------+------------+ | USER | PERMISSION | +-------+------------+ | Alice | Read | | | Write | +-------+------------+ | Bob | Read | +-------+------------+ ``` **Output (Hierarchical):** ``` +---------+----------+------+ | GROUP | SUBGROUP | ITEM | +---------+----------+------+ | Tech | CPU | i7 | | | | i9 | | +----------+------+ | | RAM | 16GB | +---------+----------+------+ | Office | CPU | i5 | +---------+----------+------+ ``` **Key Changes**: - **Method**: `SetAutoMergeCells` is replaced by `WithRowMergeMode(int_merge_mode)` or `Config.Row.Formatting.MergeMode` (`config.go`). Uses `tw.Merge...` constants. - **Merge Modes**: `tw.MergeMode` constants (`tw.MergeNone`, `tw.MergeHorizontal`, `tw.MergeVertical`, `tw.MergeHierarchical`) define behavior (`tw/tw.go`). - **Section-Specific**: Merging can be applied to `Header`, `Row`, or `Footer` via `Config.
.Formatting.MergeMode` (`tw/cell.go`). - **Processing**: Merging is handled during content preparation (`zoo.go:prepareWithMerges`, `zoo.go:applyVerticalMerges`, `zoo.go:applyHierarchicalMerges`). - **Width Adjustment**: Horizontal merging adjusts column widths (`zoo.go:applyHorizontalMergeWidths`). - **Renderer Support**: `tw.MergeState` in `tw.CellContext` ensures correct border drawing for merged cells (`tw/cell.go:CellContext`). - **Streaming Limitation**: Streaming mode supports only simple horizontal merging due to fixed widths (`stream.go:streamAppendRow`). **Migration Tips**: - Replace `SetAutoMergeCells(true)` with `WithRowMergeMode(tw.MergeHorizontal)` to maintain v0.0.5’s horizontal merging behavior (`config.go`). - Use `tw.MergeVertical` for vertical grouping (e.g., repeated user names) or `tw.MergeHierarchical` for nested data structures (`tw/tw.go`). - Apply merging to specific sections via `ConfigBuilder.
().Formatting().WithMergeMode(int_merge_mode)` (`config.go`). - Test merging with sample data to verify visual output, especially for hierarchical merging with complex datasets. - In streaming mode, ensure `Config.Widths` supports merged cell widths to avoid truncation (`stream.go`). - Use `WithDebug(true)` to log merge processing for troubleshooting (`config.go`). **Potential Pitfalls**: - **Streaming Restrictions**: Vertical and hierarchical merging are unsupported in streaming mode; use batch mode for these features (`stream.go`). - **Width Misalignment**: Merged cells may require wider columns; adjust `Config.Widths` or `ColMaxWidths` (`zoo.go`). - **Data Dependency**: Merging requires identical content; case or whitespace differences prevent merging (`zoo.go`). - **Renderer Errors**: Incorrect `tw.MergeState` handling in custom renderers can break merged cell borders; test thoroughly (`tw/cell.go`). - **Performance**: Hierarchical merging with large datasets may slow rendering; optimize data or limit merging (`zoo.go`). ### 6. Table Captions Captions add descriptive text above or below the table, a new feature in v1.0.x not present in v0.0.5. **Old (v0.0.5):** ```go package main // No direct caption support in v0.0.5. Users might have printed text manually. // import "github.com/olekukonko/tablewriter" // import "os" // import "fmt" func main() { // fmt.Println("Movie ratings.") // Manual caption // table := tablewriter.NewWriter(os.Stdout) // table.SetHeader([]string{"Name", "Sign", "Rating"}) // table.Render() } ``` **New (v1.0.x):** ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { table := tablewriter.NewTable(os.Stdout) table.Caption(tw.Caption{ // tw/types.go:Caption Text: "System Status Overview - A Very Long Caption Example To Demonstrate Wrapping Behavior", Spot: tw.SpotTopCenter, // Placement: TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight Align: tw.AlignCenter, // Text alignment within caption width Width: 30, // Fixed width for caption text wrapping; if 0, wraps to table width }) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Append("SuperLongNodeNameHere", "ActiveNow") table.Render() } ``` **Output:** ``` System Status Overview - A Very Long Caption Example To Demonst… ┌─────────────────────┬──────────┐ │ NAME │ STATUS │ ├─────────────────────┼──────────┤ │ Node1 │ Ready │ │ SuperLongNodeNameHe │ ActiveNo │ │ re │ w │ └─────────────────────┴──────────┘ ``` **Key Changes**: - **New Feature**: `Table.Caption(tw.Caption)` introduces captions, absent in v0.0.5 (`tablewriter.go:Caption`). - **Configuration**: `tw.Caption` (`tw/types.go:Caption`) includes: - `Text`: Caption content. - `Spot`: Placement (`tw.SpotTopLeft`, `tw.SpotBottomCenter`, etc.); defaults to `tw.SpotBottomCenter` if `tw.SpotNone`. - `Align`: Text alignment (`tw.AlignLeft`, `tw.AlignCenter`, `tw.AlignRight`). - `Width`: Optional fixed width for wrapping; defaults to table width. - **Rendering**: Captions are rendered by `printTopBottomCaption` before or after the table based on `Spot` (`tablewriter.go:printTopBottomCaption`). - **Streaming**: Captions are rendered during `Close()` in streaming mode if placed at the bottom (`stream.go`). **Migration Tips**: - Add captions to enhance table context, especially for reports or documentation (`tw/types.go`). - Use `tw.SpotTopCenter` for prominent placement above the table, aligning with common report formats. - Set `Align` to match table aesthetics (e.g., `tw.AlignCenter` for balanced appearance). - Specify `Width` for consistent wrapping, especially with long captions or narrow tables (`tablewriter.go`). - Test caption placement and alignment with different table sizes to ensure readability. **Potential Pitfalls**: - **Spot Default**: If `Spot` is `tw.SpotNone`, caption defaults to `tw.SpotBottomCenter`, which may surprise users expecting no caption (`tablewriter.go`). - **Width Overflow**: Without `Width`, captions wrap to table width, potentially causing misalignment; set explicitly for control (`tw/types.go`). - **Streaming Delay**: Bottom-placed captions in streaming mode appear only at `Close()`; ensure `Close()` is called (`stream.go`). - **Alignment Confusion**: Caption `Align` is independent of table cell alignment; verify separately (`tw/cell.go`). ### 7. Filters Filters allow dynamic transformation of cell content during rendering, a new feature in v1.0.x for tasks like formatting, coloring, or sanitizing data. **Old (v0.0.5):** ```go package main // No direct support for cell content transformation in v0.0.5. // Users would typically preprocess data before appending. // import "github.com/olekukonko/tablewriter" // import "os" // import "strings" func main() { // table := tablewriter.NewWriter(os.Stdout) // table.SetHeader([]string{"Name", "Status"}) // status := " Ready " // preprocessedStatus := "Status: " + strings.TrimSpace(status) // table.Append([]string{"Node1", preprocessedStatus}) // table.Render() } ``` **New (v1.0.x):** ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" // For tw.CellConfig etc. "os" "strings" ) func main() { // Per-Column Filter for Specific Transformations cfgBuilder := tablewriter.NewConfigBuilder() cfgBuilder.Row().Filter().WithPerColumn([]func(string) string{ nil, // No filter for Name column func(s string) string { // Status column: prefix and trim return "Status: " + strings.TrimSpace(s) }, }) tableWithFilter := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfgBuilder.Build())) tableWithFilter.Header("Name", "Status") tableWithFilter.Append("Node1", " Ready ") // Note the extra spaces tableWithFilter.Append("Node2", "Pending") tableWithFilter.Render() // Global filter example (applied to all cells in the Row section) cfgGlobalFilter := tablewriter.NewConfigBuilder() cfgGlobalFilter.Row().Filter().WithGlobal(func(s string) string { return "[" + s + "]" // Wrap all row cells in brackets }) tableGlobalFilter := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfgGlobalFilter.Build())) tableGlobalFilter.Header("Item", "Count") tableGlobalFilter.Append("Apple", "5") tableGlobalFilter.Render() } ``` **Output (Per-Column Filter):** ``` ┌───────┬─────────────────┐ │ NAME │ STATUS │ ├───────┼─────────────────┤ │ Node1 │ Status: Ready │ │ Node2 │ Status: Pending │ └───────┴─────────────────┘ ``` **Output (Global Filter):** ``` ┌───────┬─────────┐ │ ITEM │ COUNT │ ├───────┼─────────┤ │[Apple]│ [5] │ └───────┴─────────┘ ``` **Key Changes**: - **New Feature**: `tw.CellFilter` (`tw/cell.go:CellFilter`) introduces: - `Global`: A `func(s []string) []string` applied to entire rows (all cells in that row) of a section. - `PerColumn`: A slice of `func(string) string` for column-specific transformations on individual cells. - **Configuration**: Set via `Config.
.Filter` (`Header`, `Row`, `Footer`) using `ConfigBuilder` or direct `Config` (`config.go`). - **Processing**: Filters are applied during content preparation, after `AutoFormat` but before rendering (`zoo.go:convertCellsToStrings` calls `prepareContent` which applies some transformations, filters are applied in `convertCellsToStrings` itself). - **Use Cases**: Formatting (e.g., uppercase, prefixes), coloring (via ANSI codes), sanitization (e.g., removing sensitive data), or data normalization. **Migration Tips**: - Use filters to replace manual content preprocessing in v0.0.5 (e.g., string manipulation before `Append`). - Apply `Global` filters for uniform transformations across all cells of rows in a section (e.g., uppercasing all row data) (`tw/cell.go`). - Use `PerColumn` filters for column-specific formatting (e.g., adding prefixes to status columns) (`config.go`). - Combine filters with `tw.Formatter` for complex types or ANSI coloring for visual enhancements (`tw/cell.go`). - Test filters with diverse inputs to ensure transformations preserve data integrity (`zoo.go`). **Potential Pitfalls**: - **Filter Order**: Filters apply before some other transformations like padding and alignment; combining can lead to interactions. - **Performance**: Complex filters on large datasets may slow rendering; optimize logic (`zoo.go`). - **Nil Filters**: Unset filters (`nil`) are ignored, but incorrect indexing in `PerColumn` can skip columns (`tw/cell.go`). - **Streaming Consistency**: Filters must be consistent in streaming mode, as widths are fixed at `Start()` (`stream.go`). ### 8. Stringers and Caching Stringers allow custom string conversion for data types, with v1.0.x adding caching for performance. **Old (v0.0.5):** ```go package main // v0.0.5 primarily relied on fmt.Stringer for custom types. // import "fmt" // import "github.com/olekukonko/tablewriter" // import "os" // type MyCustomType struct { // Value string // } // func (m MyCustomType) String() string { return "Formatted: " + m.Value } func main() { // table := tablewriter.NewWriter(os.Stdout) // table.SetHeader([]string{"Custom Data"}) // table.Append([]string{MyCustomType{"test"}.String()}) // Manual call to String() // table.Render() } ``` **New (v1.0.x):** ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" // For tw.Formatter "os" "strings" // For Person example ) // Example 1: Using WithStringer for general conversion type CustomInt int func main() { // Table with a general stringer (func(any) []string) tableWithStringer := tablewriter.NewTable(os.Stdout, tablewriter.WithStringer(func(v any) []string { // Must return []string if ci, ok := v.(CustomInt); ok { return []string{fmt.Sprintf("CustomInt Value: %d!", ci)} } return []string{fmt.Sprintf("Value: %v", v)} // Fallback }), tablewriter.WithDebug(true), // Enable caching if WithStringerCache() is also used // tablewriter.WithStringerCache(), // Optional: enable caching ) tableWithStringer.Header("Data") tableWithStringer.Append(123) tableWithStringer.Append(CustomInt(456)) tableWithStringer.Render() fmt.Println("\n--- Table with Type-Specific Stringer for Structs ---") // Example 2: Stringer for a specific struct type type Person struct { ID int Name string City string } // Stringer for Person to produce 3 cells personToStrings := func(p Person) []string { return []string{ fmt.Sprintf("ID: %d", p.ID), p.Name, strings.ToUpper(p.City), } } tablePersonStringer := tablewriter.NewTable(os.Stdout, tablewriter.WithStringer(personToStrings), // Pass the type-specific function ) tablePersonStringer.Header("User ID", "Full Name", "Location") tablePersonStringer.Append(Person{1, "Alice", "New York"}) tablePersonStringer.Append(Person{2, "Bob", "London"}) tablePersonStringer.Render() fmt.Println("\n--- Table with tw.Formatter ---") // Example 3: Using tw.Formatter for types type Product struct { Name string Price float64 } func (p Product) Format() string { // Implements tw.Formatter return fmt.Sprintf("%s - $%.2f", p.Name, p.Price) } tableFormatter := tablewriter.NewTable(os.Stdout) tableFormatter.Header("Product Details") tableFormatter.Append(Product{"Laptop", 1200.99}) // Will use Format() tableFormatter.Render() } ``` **Output (Stringer Examples):** ``` ┌─────────────────────┐ │ DATA │ ├─────────────────────┤ │ Value: 123 │ │ CustomInt Value: 456! │ └─────────────────────┘ --- Table with Type-Specific Stringer for Structs --- ┌─────────┬───────────┬──────────┐ │ USER ID │ FULL NAME │ LOCATION │ ├─────────┼───────────┼──────────┤ │ ID: 1 │ Alice │ NEW YORK │ │ ID: 2 │ Bob │ LONDON │ └─────────┴───────────┴──────────┘ --- Table with tw.Formatter --- ┌─────────────────┐ │ PRODUCT DETAILS │ ├─────────────────┤ │ Laptop - $1200… │ └─────────────────┘ ``` **Key Changes**: - **Stringer Support**: `WithStringer(fn any)` sets a table-wide string conversion function. This function must have a signature like `func(SomeType) []string` or `func(any) []string`. It's used to convert an input item (e.g., a struct) into a slice of strings, where each string is a cell for the row (`tablewriter.go:WithStringer`). - **Caching**: `WithStringerCache()` enables caching for the function provided via `WithStringer`, improving performance for repeated conversions of the same input type (`tablewriter.go:WithStringerCache`). - **Formatter**: `tw.Formatter` interface (`Format() string`) allows types to define their own single-string representation for a cell. This is checked before `fmt.Stringer` (`tw/types.go:Formatter`). - **Priority**: When converting an item to cell(s): 1. `WithStringer` (if provided and compatible with the item's type). 2. If not handled by `WithStringer`, or if `WithStringer` is not set: * If the item is a struct that does *not* implement `tw.Formatter` or `fmt.Stringer`, its exported fields are reflected into multiple cells. * If the item (or struct) implements `tw.Formatter` (`Format() string`), that's used for a single cell. * Else, if it implements `fmt.Stringer` (`String() string`), that's used for a single cell. * Else, default `fmt.Sprintf("%v", ...)` for a single cell. (`zoo.go:convertCellsToStrings`, `zoo.go:convertItemToCells`) **Migration Tips**: - For types that should produce a single cell with custom formatting, implement `tw.Formatter`. - For types (especially structs) that should be expanded into multiple cells with custom logic, use `WithStringer` with a function like `func(MyType) []string`. - If you have a general way to convert *any* type into a set of cells, use `WithStringer(func(any) []string)`. - Enable `WithStringerCache` for large datasets with repetitive data types if using `WithStringer`. - Test stringer/formatter output to ensure formatting meets expectations (`zoo.go`). **Potential Pitfalls**: - **Cache Overhead**: `WithStringerCache` may increase memory usage for diverse data; disable for small tables or if not using `WithStringer` (`tablewriter.go`). - **Stringer Signature**: The function passed to `WithStringer` *must* return `[]string`. A function returning `string` will lead to a warning and fallback behavior. - **Formatter vs. Stringer Priority**: Be aware of the conversion priority if your types implement multiple interfaces or if you also use `WithStringer`. - **Streaming**: Stringers/Formatters must produce consistent cell counts in streaming mode to maintain width alignment (`stream.go`). ## Examples This section provides practical examples to demonstrate v1.0.x features, covering common and advanced use cases to aid migration. Each example includes code, output, and notes to illustrate functionality. ### Example: Minimal Setup A basic table with default settings, ideal for quick setups. ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { table := tablewriter.NewTable(os.Stdout) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Render() } ``` **Output**: ``` ┌───────┬────────┐ │ NAME │ STATUS │ ├───────┼────────┤ │ Node1 │ Ready │ └───────┴────────┘ ``` **Notes**: - Uses default `renderer.NewBlueprint()` and `defaultConfig()` settings (`tablewriter.go`, `config.go`). - `Header: tw.AlignCenter`, `Row: tw.AlignLeft` (`config.go:defaultConfig`). - Simple replacement for v0.0.5’s `NewWriter` and `SetHeader`/`Append`. ### Example: Streaming with Fixed Widths Demonstrates streaming mode for real-time data output. ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "log" "os" ) func main() { table := tablewriter.NewTable(os.Stdout, tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), tablewriter.WithColumnMax(10), // Sets Config.Widths.Global ) if err := table.Start(); err != nil { log.Fatalf("Start failed: %v", err) } table.Header("Name", "Status") for i := 0; i < 2; i++ { err := table.Append(fmt.Sprintf("Node%d", i+1), "Ready") if err != nil { log.Printf("Append failed: %v", err) } } if err := table.Close(); err != nil { log.Fatalf("Close failed: %v", err) } } ``` **Output**: ``` ┌──────────┬──────────┐ │ NAME │ STATUS │ ├──────────┼──────────┤ │ Node1 │ Ready │ │ Node2 │ Ready │ └──────────┴──────────┘ ``` **Notes**: - Streaming requires `Start()` and `Close()`; `Config.Widths` (here set via `WithColumnMax`) fixes widths (`stream.go`). - Replaces v0.0.5’s batch rendering for real-time use cases. - Ensure error handling for `Start()`, `Append()`, and `Close()`. ### Example: Markdown-Style Table Creates a Markdown-compatible table for documentation. ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" // Import renderer "github.com/olekukonko/tablewriter/tw" "os" ) func main() { // Example 1: Using Blueprint renderer with Markdown symbols tableBlueprintMarkdown := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), // Use Blueprint tablewriter.WithRendition(tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleMarkdown), Borders: tw.Border{Left: tw.On, Right: tw.On}, // Markdown needs left/right borders }), tablewriter.WithRowAlignment(tw.AlignLeft), // Common for Markdown tablewriter.WithHeaderAlignment(tw.AlignCenter), // Center align headers ) tableBlueprintMarkdown.Header("Name", "Status") tableBlueprintMarkdown.Append("Node1", "Ready") tableBlueprintMarkdown.Append("Node2", "NotReady") tableBlueprintMarkdown.Render() fmt.Println("\n--- Using dedicated Markdown Renderer (if one exists or is built) ---") // Example 2: Assuming a dedicated Markdown renderer (hypothetical example) // If a `renderer.NewMarkdown()` existed that directly outputs GitHub Flavored Markdown table syntax: /* tableDedicatedMarkdown := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewMarkdown()), // Hypothetical Markdown renderer ) tableDedicatedMarkdown.Header("Name", "Status") tableDedicatedMarkdown.Append("Node1", "Ready") tableDedicatedMarkdown.Append("Node2", "NotReady") tableDedicatedMarkdown.Render() */ // Since `renderer.NewMarkdown()` isn't shown in the provided code, // the first example (Blueprint with StyleMarkdown) is the current viable way. } ``` **Output (Blueprint with StyleMarkdown):** ``` | NAME | STATUS | |--------|----------| | Node1 | Ready | | Node2 | NotReady | ``` **Notes**: - `StyleMarkdown` ensures compatibility with Markdown parsers (`tw/symbols.go`). - Left alignment for rows and center for headers is common for Markdown readability (`config.go`). - Ideal for GitHub READMEs or documentation. - A dedicated Markdown renderer (like the commented-out example) would typically handle alignment syntax (e.g., `|:---:|:---|`). With `Blueprint` and `StyleMarkdown`, alignment is visual within the text rather than Markdown syntax. ### Example: ASCII-Style Table Uses `StyleASCII` for maximum terminal compatibility. ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), tablewriter.WithRendition(tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleASCII), }), tablewriter.WithRowAlignment(tw.AlignLeft), ) table.Header("ID", "Value") table.Append("1", "Test") table.Render() } ``` **Output**: ``` +----+-------+ │ ID │ VALUE │ +----+-------+ │ 1 │ Test │ +----+-------+ ``` **Notes**: - `StyleASCII` is robust for all terminals (`tw/symbols.go`). - Replaces v0.0.5’s default style with explicit configuration. ### Example: Kubectl-Style Output Creates a borderless, minimal table similar to `kubectl` command output, emphasizing simplicity and readability. ```go package main import ( "os" "sync" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) var wg sync.WaitGroup func main() { data := [][]any{ {"node1.example.com", "Ready", "compute", "1.11"}, {"node2.example.com", "Ready", "compute", "1.11"}, {"node3.example.com", "Ready", "compute", "1.11"}, {"node4.example.com", "NotReady", "compute", "1.11"}, } table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Borders: tw.BorderNone, Settings: tw.Settings{ Separators: tw.SeparatorsNone, Lines: tw.LinesNone, }, })), tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{Alignment: tw.AlignLeft}, }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{Alignment: tw.AlignLeft}, Padding: tw.CellPadding{Global: tw.PaddingNone}, }, }), ) table.Header("Name", "Status", "Role", "Version") table.Bulk(data) table.Render() } ``` **Output:** ``` NAME STATUS ROLE VERSION node1.example.com Ready compute 1.21.3 node2.example.com Ready infra 1.21.3 node3.example.com NotReady compute 1.20.7 ``` **Notes**: - **Configuration**: Uses `tw.BorderNone`, `tw.LinesNone`, `tw.SeparatorsNone`, `tw.NewSymbols(tw.StyleNone)` and specific padding (`Padding{Right:" "}`) for a minimal, borderless layout. - **Migration from v0.0.5**: Replaces `SetBorder(false)` and manual spacing with `tw.Rendition` and `Config` settings, achieving a cleaner kubectl-like output. - **Key Features**: - Left-aligned text for readability (`config.go`). - `WithTrimSpace(tw.Off)` preserves spacing (`config.go`). - `Bulk` efficiently adds multiple rows (`tablewriter.go`). - Padding `Right: " "` is used to create space between columns as separators are off. - **Best Practices**: Test in terminals to ensure spacing aligns with command-line aesthetics; use `WithDebug(true)` for layout issues (`config.go`). - **Potential Issues**: Column widths are content-based. For very long content in one column and short in others, it might not look perfectly aligned like fixed-width CLI tools. `Config.Widths` could be used for more control if needed. ### Example: Hierarchical Merging Demonstrates hierarchical cell merging for nested data structures, a new feature in v1.0.x. ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { data := [][]string{ // Header row is separate {"table\nwriter", "v0.0.1", "legacy"}, {"table\nwriter", "v0.0.2", "legacy"}, {"table\nwriter", "v0.0.2", "legacy"}, // Duplicate for testing merge {"table\nwriter", "v0.0.2", "legacy"}, // Duplicate for testing merge {"table\nwriter", "v0.0.5", "legacy"}, {"table\nwriter", "v1.0.6", "latest"}, } rendition := tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleLight), // Use light for clearer merge lines Settings: tw.Settings{ Separators: tw.Separators{BetweenRows: tw.On}, Lines: tw.Lines{ShowHeaderLine: tw.On, ShowFooterLine: tw.On}, // Show header line }, Borders: tw.Border{Left:tw.On, Right:tw.On, Top:tw.On, Bottom:tw.On}, } tableHier := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), tablewriter.WithRendition(rendition), tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHierarchical, // Alignment: tw.AlignCenter, // Default is Left, often better for hierarchical AutoWrap: tw.WrapNormal, // Allow wrapping for "table\nwriter" }, }, }), ) tableHier.Header("Package", "Version", "Status") // Header tableHier.Bulk(data) // Bulk data tableHier.Render() // --- Vertical Merging Example for Contrast --- tableVert := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), tablewriter.WithRendition(rendition), // Reuse same rendition tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeVertical, AutoWrap: tw.WrapNormal, }, }, }), ) tableVert.Header("Package", "Version", "Status") tableVert.Bulk(data) tableVert.Render() } ``` **Output (Hierarchical):** ``` ┌─────────┬─────────┬────────┐ │ PACKAGE │ VERSION │ STATUS │ ├─────────┼─────────┼────────┤ │ table │ v0.0.1 │ legacy │ │ writer │ │ │ │ ├─────────┼────────┤ │ │ v0.0.2 │ legacy │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├─────────┼────────┤ │ │ v0.0.5 │ legacy │ │ ├─────────┼────────┤ │ │ v1.0.6 │ latest │ └─────────┴─────────┴────────┘ ``` **Output (Vertical):** ``` ┌─────────┬─────────┬────────┐ │ PACKAGE │ VERSION │ STATUS │ ├─────────┼─────────┼────────┤ │ table │ v0.0.1 │ legacy │ │ writer │ │ │ │ ├─────────┤ │ │ │ v0.0.2 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├─────────┤ │ │ │ v0.0.5 │ │ │ ├─────────┼────────┤ │ │ v1.0.6 │ latest │ └─────────┴─────────┴────────┘ ``` **Notes**: - **Configuration**: Uses `tw.MergeHierarchical` to merge cells based on matching values in preceding columns (`tw/tw.go`). - **Migration from v0.0.5**: Extends `SetAutoMergeCells(true)` (horizontal only) with hierarchical merging for complex data (`tablewriter.go`). - **Key Features**: - `StyleLight` for clear visuals (`tw/symbols.go`). - `Bulk` for efficient data loading (`tablewriter.go`). - **Best Practices**: Test merging with nested data; use batch mode, as streaming doesn’t support hierarchical merging (`stream.go`). - **Potential Issues**: Ensure data is sorted appropriately for hierarchical merging to work as expected; mismatches prevent merging (`zoo.go`). `AutoWrap: tw.WrapNormal` helps with multi-line cell content like "table\nwriter". ### Example: Colorized Table Applies ANSI colors to highlight status values, replacing v0.0.5’s `SetColumnColor`. ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" // For tw.State, tw.CellConfig etc. "os" ) func main() { const ( FgGreen = "\033[32m" FgRed = "\033[31m" Reset = "\033[0m" ) cfgBuilder := tablewriter.NewConfigBuilder() cfgBuilder.Row().Filter().WithPerColumn([]func(string) string{ nil, // No filter for Name func(s string) string { // Color Status if s == "Ready" { return FgGreen + s + Reset } return FgRed + s + Reset }, }) table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfgBuilder.Build())) table.Header("Name", "Status") table.Append("Node1", "Ready") table.Append("Node2", "Error") table.Render() } ``` **Output (Text Approximation, Colors Not Shown):** ``` ┌───────┬────────┐ │ NAME │ STATUS │ ├───────┼────────┤ │ Node1 │ Ready │ │ Node2 │ Error │ └───────┴────────┘ ``` **Notes**: - **Configuration**: Uses `tw.CellFilter` for per-column coloring, embedding ANSI codes (`tw/cell.go`). - **Migration from v0.0.5**: Replaces `SetColumnColor` with dynamic filters (`tablewriter.go`). - **Key Features**: Flexible color application; `twdw.Width` handles ANSI codes correctly (`tw/fn.go`). - **Best Practices**: Test in ANSI-compatible terminals; use constants for code clarity. - **Potential Issues**: Non-ANSI terminals may show artifacts; provide fallbacks (`tw/fn.go`). ### Example: Vertical Merging Shows vertical cell merging for repeated values in a column. (This example was very similar to the hierarchical one, so I'll ensure it's distinct by using simpler data). ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint()), // Default renderer tablewriter.WithRendition(tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleLight), Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}}, Borders: tw.Border{Left:tw.On, Right:tw.On, Top:tw.On, Bottom:tw.On}, }), tablewriter.WithConfig( tablewriter.NewConfigBuilder(). Row().Formatting().WithMergeMode(tw.MergeVertical).Build(). // Enable Vertical Merge Build(), ), ) table.Header("User", "Permission", "Target") table.Append("Alice", "Read", "FileA") table.Append("Alice", "Write", "FileA") // Alice and FileA will merge vertically table.Append("Alice", "Read", "FileB") table.Append("Bob", "Read", "FileA") table.Append("Bob", "Read", "FileC") // Bob and Read will merge table.Render() } ``` **Output:** ``` ┌───────┬────────────┬────────┐ │ USER │ PERMISSION │ TARGET │ ├───────┼────────────┼────────┤ │ Alice │ Read │ FileA │ │ │ │ │ │ ├────────────┤ │ │ │ Write │ │ │ ├────────────┼────────┤ │ │ Read │ FileB │ ├───────┼────────────┼────────┤ │ Bob │ Read │ FileA │ │ │ ├────────┤ │ │ │ FileC │ └───────┴────────────┴────────┘ ``` **Notes**: - **Configuration**: `tw.MergeVertical` merges identical cells vertically (`tw/tw.go`). - **Migration from v0.0.5**: Extends `SetAutoMergeCells` with vertical merging (`tablewriter.go`). - **Key Features**: Enhances grouped data display; requires batch mode (`stream.go`). - **Best Practices**: Sort data by the columns you intend to merge for best results; test with `StyleLight` for clarity (`tw/symbols.go`). - **Potential Issues**: Streaming doesn’t support vertical merging; use batch mode (`stream.go`). ### Example: Custom Renderer (CSV Output) Implements a custom renderer for CSV output. ```go package main import ( "fmt" "github.com/olekukonko/ll" // For logger type "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "io" "os" "strings" // For CSV escaping ) // CSVRenderer implements tw.Renderer type CSVRenderer struct { writer io.Writer config tw.Rendition // Store the rendition logger *ll.Logger err error } func (r *CSVRenderer) Start(w io.Writer) error { r.writer = w return nil // No initial output for CSV typically } func (r *CSVRenderer) escapeCSVCell(data string) string { // Basic CSV escaping: double quotes if it contains comma, newline, or quote if strings.ContainsAny(data, ",\"\n") { return `"` + strings.ReplaceAll(data, `"`, `""`) + `"` } return data } func (r *CSVRenderer) writeLine(cells map[int]tw.CellContext, numCols int) { if r.err != nil { return } var lineParts []string // Need to iterate in column order for CSV keys := make([]int, 0, len(cells)) for k := range cells { keys = append(keys, k) } // This simple sort works for int keys 0,1,2... // For more complex scenarios, a proper sort might be needed if keys aren't sequential. for i := 0; i < numCols; i++ { // Assume numCols reflects the intended max columns if cellCtx, ok := cells[i]; ok { lineParts = append(lineParts, r.escapeCSVCell(cellCtx.Data)) } else { lineParts = append(lineParts, "") // Empty cell if not present } } _, r.err = r.writer.Write([]byte(strings.Join(lineParts, ",") + "\n")) } func (r *CSVRenderer) Header(headers [][]string, ctx tw.Formatting) { // For CSV, usually only the first line of headers is relevant // The ctx.Row.Current will contain the cells for the first line of the header being processed r.writeLine(ctx.Row.Current, len(ctx.Row.Current)) } func (r *CSVRenderer) Row(row []string, ctx tw.Formatting) { // ctx.Row.Current contains the cells for the current row line r.writeLine(ctx.Row.Current, len(ctx.Row.Current)) } func (r *CSVRenderer) Footer(footers [][]string, ctx tw.Formatting) { // Similar to Header/Row, using ctx.Row.Current for the footer line data r.writeLine(ctx.Row.Current, len(ctx.Row.Current)) } func (r *CSVRenderer) Line(ctx tw.Formatting) { /* No separator lines in CSV */ } func (r *CSVRenderer) Close() error { return r.err } func (r *CSVRenderer) Config() tw.Rendition { return r.config } func (r *CSVRenderer) Logger(logger *ll.Logger) { r.logger = logger } func main() { table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(&CSVRenderer{ // config can be minimal for CSV as symbols/borders aren't used config: tw.Rendition{}, })) table.Header("Name", "Status", "Notes, with comma") table.Append("Node1", "Ready", "All systems \"go\"!") table.Append("Node2", "Error", "Needs\nattention") table.Footer("Summary", "2 Nodes", "Check logs") table.Render() } ``` **Output:** ```csv Name,Status,"Notes, with comma" Node1,Ready,"All systems ""go""!" Node2,Error,"Needs attention" Summary,2 Nodes,Check logs ``` **Notes**: - **Configuration**: Custom `CSVRenderer` implements `tw.Renderer` for CSV output (`tw/renderer.go`). - **Migration from v0.0.5**: Extends v0.0.5’s text-only output with custom formats (`tablewriter.go`). - **Key Features**: Handles basic CSV cell escaping; supports streaming if `Append` is called multiple times. `ctx.Row.Current` (map[int]tw.CellContext) is used to access cell data. - **Best Practices**: Test with complex data (e.g., commas, quotes, newlines); implement all renderer methods. A more robust CSV renderer would use the `encoding/csv` package. - **Potential Issues**: Custom renderers require careful error handling and correct interpretation of `tw.Formatting` and `tw.RowContext`. This example's `writeLine` assumes columns are 0-indexed and contiguous for simplicity. ## Troubleshooting and Common Pitfalls This section addresses common migration issues with detailed solutions, covering 30+ scenarios to reduce support tickets. | Issue | Cause/Solution | |-------------------------------------------|-------------------------------------------------------------------------------| | No output from `Render()` | **Cause**: Missing `Start()`/`Close()` in streaming mode or invalid `io.Writer`. **Solution**: Ensure `Start()` and `Close()` are called in streaming; verify `io.Writer` (`stream.go`). | | Incorrect column widths | **Cause**: Missing `Config.Widths` in streaming or content-based sizing. **Solution**: Set `Config.Widths` before `Start()`; use `WithColumnMax` (`stream.go`). | | Merging not working | **Cause**: Streaming mode or mismatched data. **Solution**: Use batch mode for vertical/hierarchical merging; ensure identical content (`zoo.go`). | | Alignment ignored | **Cause**: `PerColumn` overrides `Global`. **Solution**: Check `Config.Section.Alignment.PerColumn` settings or `ConfigBuilder` calls (`tw/cell.go`). | | Padding affects widths | **Cause**: Padding included in `Config.Widths`. **Solution**: Adjust `Config.Widths` to account for `tw.CellPadding` (`zoo.go`). | | Colors not rendering | **Cause**: Non-ANSI terminal or incorrect codes. **Solution**: Test in ANSI-compatible terminal; use `twdw.Width` (`tw/fn.go`). | | Caption missing | **Cause**: `Close()` not called in streaming or incorrect `Spot`. **Solution**: Ensure `Close()`; verify `tw.Caption.Spot` (`tablewriter.go`). | | Filters not applied | **Cause**: Incorrect `PerColumn` indexing or nil filters. **Solution**: Set filters correctly; test with sample data (`tw/cell.go`). | | Stringer cache overhead | **Cause**: Large datasets with diverse types. **Solution**: Disable `WithStringerCache` for small tables if not using `WithStringer` or if types vary greatly (`tablewriter.go`). | | Deprecated methods used | **Cause**: Using `WithBorders`, old `tablewriter.Behavior` constants. **Solution**: Migrate to `WithRendition`, `tw.Behavior` struct (`tablewriter.go`, `deprecated.go`). | | Streaming footer missing | **Cause**: `Close()` not called. **Solution**: Always call `Close()` (`stream.go`). | | Hierarchical merging fails | **Cause**: Unsorted data or streaming mode. **Solution**: Sort data; use batch mode (`zoo.go`). | | Custom renderer errors | **Cause**: Incomplete method implementation or misinterpreting `tw.Formatting`. **Solution**: Implement all `tw.Renderer` methods; test thoroughly (`tw/renderer.go`). | | Width overflow | **Cause**: No `MaxWidth` or wide content. **Solution**: Set `Config.MaxWidth` (`config.go`). | | Truncated content | **Cause**: Narrow `Config.Widths` or `tw.WrapTruncate`. **Solution**: Widen columns or use `tw.WrapNormal` (`zoo.go`). | | Debug logs absent | **Cause**: `Debug = false`. **Solution**: Enable `WithDebug(true)` (`config.go`). | | Alignment mismatch across sections | **Cause**: Different defaults. **Solution**: Set uniform alignment options (e.g., via `ConfigBuilder.
.Alignment()`) (`config.go`). | | ANSI code artifacts | **Cause**: Non-ANSI terminal. **Solution**: Provide non-colored fallback (`tw/fn.go`). | | Slow rendering | **Cause**: Complex filters or merging. **Solution**: Optimize logic; limit merging (`zoo.go`). | | Uneven cell counts | **Cause**: Mismatched rows/headers. **Solution**: Pad with `""` (`zoo.go`). | | Border inconsistencies | **Cause**: Mismatched `Borders`/`Symbols`. **Solution**: Align settings (`tw/renderer.go`). | | Streaming width issues | **Cause**: No `Config.Widths`. **Solution**: Set before `Start()` (`stream.go`). | | Formatter ignored | **Cause**: `WithStringer` might take precedence if compatible. **Solution**: Review conversion priority; `tw.Formatter` is high-priority for single-item-to-single-cell conversion (`zoo.go`). | | Caption misalignment | **Cause**: Incorrect `Width` or `Align`. **Solution**: Set `tw.Caption.Width`/`Align` (`tablewriter.go`). | | Per-column padding errors | **Cause**: Incorrect indexing in `Padding.PerColumn`. **Solution**: Verify indices (`tw/cell.go`). | | Vertical merging in streaming | **Cause**: Unsupported. **Solution**: Use batch mode (`stream.go`). | | Filter performance | **Cause**: Complex logic. **Solution**: Simplify filters (`zoo.go`). | | Custom symbols incomplete | **Cause**: Missing characters. **Solution**: Define all symbols (`tw/symbols.go`). | | Table too wide | **Cause**: No `MaxWidth`. **Solution**: Set `Config.MaxWidth` (`config.go`). | | Streaming errors | **Cause**: Missing `Start()`. **Solution**: Call `Start()` before data input (`stream.go`). | ## Additional Notes - **Performance Optimization**: Enable `WithStringerCache` for repetitive data types when using `WithStringer`; optimize filters and merging for large datasets (`tablewriter.go`, `zoo.go`). - **Debugging**: Use `WithDebug(true)` and `table.Debug()` to log configuration and rendering details; invaluable for troubleshooting (`config.go`). - **Testing Resources**: The `tests/` directory contains examples of various configurations. - **Community Support**: For advanced use cases or issues, consult the source code or open an issue on the `tablewriter` repository. - **Future Considerations**: Deprecated methods in `deprecated.go` (e.g., `WithBorders`) are slated for removal in future releases; migrate promptly to ensure compatibility. This guide aims to cover all migration scenarios comprehensively. For highly specific or advanced use cases, refer to the source files (`config.go`, `tablewriter.go`, `stream.go`, `tw/*`) or engage with the `tablewriter` community for support.tablewriter-1.0.9/README.md000066400000000000000000001036721504170215200153320ustar00rootroot00000000000000# TableWriter for Go [![Go](https://github.com/olekukonko/tablewriter/actions/workflows/go.yml/badge.svg)](https://github.com/olekukonko/tablewriter/actions/workflows/go.yml) [![Go Reference](https://pkg.go.dev/badge/github.com/olekukonko/tablewriter.svg)](https://pkg.go.dev/github.com/olekukonko/tablewriter) [![Go Report Card](https://goreportcard.com/badge/github.com/olekukonko/tablewriter)](https://goreportcard.com/report/github.com/olekukonko/tablewriter) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Benchmarks](https://img.shields.io/badge/benchmarks-included-success)](README.md#benchmarks) `tablewriter` is a Go library for generating **rich text-based tables** with support for multiple output formats, including ASCII, Unicode, Markdown, HTML, and colorized terminals. Perfect for CLI tools, logs, and web applications. ### Key Features - **Multi-format rendering**: ASCII, Unicode, Markdown, HTML, ANSI-colored - **Advanced styling**: Cell merging, alignment, padding, borders - **Flexible input**: CSV, structs, slices, or streaming data - **High performance**: Minimal allocations, buffer reuse - **Modern features**: Generics support, hierarchical merging, real-time streaming --- ### Installation #### Legacy Version (v0.0.5) For use with legacy applications: ```bash go get github.com/olekukonko/tablewriter@v0.0.5 ``` #### Latest Version The latest stable version ```bash go get github.com/olekukonko/tablewriter@v1.0.9 ``` **Warning:** Version `v1.0.0` contains missing functionality and should not be used. > **Version Guidance** > - Legacy: Use `v0.0.5` (stable) > - New Features: Use `@latest` (includes generics, super fast streaming APIs) > - Legacy Docs: See [README_LEGACY.md](README_LEGACY.md) --- ### Why TableWriter? - **CLI Ready**: Instant compatibility with terminal outputs - **Database Friendly**: Native support for `sql.Null*` types - **Secure**: Auto-escaping for HTML/Markdown - **Extensible**: Custom renderers and formatters --- ### Quick Example ```go package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { data := [][]string{ {"Package", "Version", "Status"}, {"tablewriter", "v0.0.5", "legacy"}, {"tablewriter", "v1.0.9", "latest"}, } table := tablewriter.NewWriter(os.Stdout) table.Header(data[0]) table.Bulk(data[1:]) table.Render() } ``` **Output**: ``` ┌─────────────┬─────────┬────────┐ │ PACKAGE │ VERSION │ STATUS │ ├─────────────┼─────────┼────────┤ │ tablewriter │ v0.0.5 │ legacy │ │ tablewriter │ v1.0.9 │ latest │ └─────────────┴─────────┴────────┘ ``` ## Detailed Usage Create a table with `NewTable` or `NewWriter`, configure it using options or a `Config` struct, add data with `Append` or `Bulk`, and render to an `io.Writer`. Use renderers like `Blueprint` (ASCII), `HTML`, `Markdown`, `Colorized`, or `Ocean` (streaming). Here's how the API primitives map to the generated ASCII table: ``` API Call ASCII Table Component -------- --------------------- table.Header([]string{"NAME", "AGE"}) ┌──────┬─────┐ ← Borders.Top │ NAME │ AGE │ ← Header row ├──────┼─────┤ ← Lines.ShowTop (header separator) table.Append([]string{"Alice", "25"}) │ Alice│ 25 │ ← Data row ├──────┼─────┤ ← Separators.BetweenRows table.Append([]string{"Bob", "30"}) │ Bob │ 30 │ ← Data row ├──────┼─────┤ ← Lines.ShowBottom (footer separator) table.Footer([]string{"Total", "2"}) │ Total│ 2 │ ← Footer row └──────┴─────┘ ← Borders.Bottom ``` The core components include: - **Renderer** - Implements the core interface for converting table data into output formats. Available renderers include Blueprint (ASCII), HTML, Markdown, Colorized (ASCII with color), Ocean (streaming ASCII), and SVG. - **Config** - The root configuration struct that controls all table behavior and appearance - **Behavior** - Controls high-level rendering behaviors including auto-hiding empty columns, trimming row whitespace, header/footer visibility, and compact mode for optimized merged cell calculations - **CellConfig** - The comprehensive configuration template used for table sections (header, row, footer). Combines formatting, padding, alignment, filtering, callbacks, and width constraints with global and per-column control - **StreamConfig** - Configuration for streaming mode including enable/disable state and strict column validation - **Rendition** - Defines how a renderer formats tables and contains the complete visual styling configuration - **Borders** - Control the outer frame visibility (top, bottom, left, right edges) of the table - **Lines** - Control horizontal boundary lines (above/below headers, above footers) that separate different table sections - **Separators** - Control the visibility of separators between rows and between columns within the table content - **Symbols** - Define the characters used for drawing table borders, corners, and junctions These components can be configured with various `tablewriter.With*()` functional options when creating a new table. ## Examples ### Basic Examples #### 1. Simple Tables Create a basic table with headers and rows. ##### default ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "os" ) type Age int func (a Age) String() string { return fmt.Sprintf("%d yrs", a) } func main() { data := [][]any{ {"Alice", Age(25), "New York"}, {"Bob", Age(30), "Boston"}, } table := tablewriter.NewTable(os.Stdout) table.Header("Name", "Age", "City") table.Bulk(data) table.Render() } ``` **Output**: ``` ┌───────┬────────┬──────────┐ │ NAME │ AGE │ CITY │ ├───────┼────────┼──────────┤ │ Alice │ 25 yrs │ New York │ │ Bob │ 30 yrs │ Boston │ └───────┴────────┴──────────┘ ``` ##### with customization ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) type Age int func (a Age) String() string { return fmt.Sprintf("%d yrs", a) } func main() { data := [][]any{ {"Alice", Age(25), "New York"}, {"Bob", Age(30), "Boston"}, } symbols := tw.NewSymbolCustom("Nature"). WithRow("~"). WithColumn("|"). WithTopLeft("🌱"). WithTopMid("🌿"). WithTopRight("🌱"). WithMidLeft("🍃"). WithCenter("❀"). WithMidRight("🍃"). WithBottomLeft("🌻"). WithBottomMid("🌾"). WithBottomRight("🌻") table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: symbols}))) table.Header("Name", "Age", "City") table.Bulk(data) table.Render() } ``` ``` 🌱~~~~~~❀~~~~~~~~❀~~~~~~~~~🌱 | NAME | AGE | CITY | 🍃~~~~~~❀~~~~~~~~❀~~~~~~~~~🍃 | Alice | 25 yrs | New York | | Bob | 30 yrs | Boston | 🌻~~~~~~❀~~~~~~~~❀~~~~~~~~~🌻 ``` See [symbols example](https://github.com/olekukonko/tablewriter/blob/master/_example/symbols/main.go) for more #### 2. Markdown Table Generate a Markdown table for documentation. ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "os" "strings" "unicode" ) type Name struct { First string Last string } // this will be ignored since Format() is present func (n Name) String() string { return fmt.Sprintf("%s %s", n.First, n.Last) } // Note: Format() overrides String() if both exist. func (n Name) Format() string { return fmt.Sprintf("%s %s", n.clean(n.First), n.clean(n.Last)) } // clean ensures the first letter is capitalized and the rest are lowercase func (n Name) clean(s string) string { s = strings.TrimSpace(strings.ToLower(s)) words := strings.Fields(s) s = strings.Join(words, "") if s == "" { return s } // Capitalize the first letter runes := []rune(s) runes[0] = unicode.ToUpper(runes[0]) return string(runes) } type Age int // Age int will be ignore and string will be used func (a Age) String() string { return fmt.Sprintf("%d yrs", a) } func main() { data := [][]any{ {Name{"Al i CE", " Ma SK"}, Age(25), "New York"}, {Name{"bOb", "mar le y"}, Age(30), "Boston"}, } table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewMarkdown()), ) table.Header([]string{"Name", "Age", "City"}) table.Bulk(data) table.Render() } ``` **Output**: ``` | NAME | AGE | CITY | |:----------:|:------:|:--------:| | Alice Mask | 25 yrs | New York | | Bob Marley | 30 yrs | Boston | ``` #### 3. CSV Input Create a table from a CSV file with custom row alignment. ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "log" "os" ) func main() { // Assuming "test.csv" contains: "First Name,Last Name,SSN\nJohn,Barry,123456\nKathy,Smith,687987" table, err := tablewriter.NewCSV(os.Stdout, "test.csv", true) if err != nil { log.Fatalf("Error: %v", err) } table.Configure(func(config *tablewriter.Config) { config.Row.Alignment.Global = tw.AlignLeft }) table.Render() } ``` **Output**: ``` ┌────────────┬───────────┬─────────┐ │ FIRST NAME │ LAST NAME │ SSN │ ├────────────┼───────────┼─────────┤ │ John │ Barry │ 123456 │ │ Kathy │ Smith │ 687987 │ └────────────┴───────────┴─────────┘ ``` ### Advanced Examples #### 4. Colorized Table with Long Values Create a colorized table with wrapped long values, per-column colors, and a styled footer (inspired by `TestColorizedLongValues` and `TestColorizedCustomColors`). ```go package main import ( "github.com/fatih/color" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { data := [][]string{ {"1", "This is a very long description that needs wrapping for readability", "OK"}, {"2", "Short description", "DONE"}, {"3", "Another lengthy description requiring truncation or wrapping", "ERROR"}, } // Configure colors: green headers, cyan/magenta rows, yellow footer colorCfg := renderer.ColorizedConfig{ Header: renderer.Tint{ FG: renderer.Colors{color.FgGreen, color.Bold}, // Green bold headers BG: renderer.Colors{color.BgHiWhite}, }, Column: renderer.Tint{ FG: renderer.Colors{color.FgCyan}, // Default cyan for rows Columns: []renderer.Tint{ {FG: renderer.Colors{color.FgMagenta}}, // Magenta for column 0 {}, // Inherit default (cyan) {FG: renderer.Colors{color.FgHiRed}}, // High-intensity red for column 2 }, }, Footer: renderer.Tint{ FG: renderer.Colors{color.FgYellow, color.Bold}, // Yellow bold footer Columns: []renderer.Tint{ {}, // Inherit default {FG: renderer.Colors{color.FgHiYellow}}, // High-intensity yellow for column 1 {}, // Inherit default }, }, Border: renderer.Tint{FG: renderer.Colors{color.FgWhite}}, // White borders Separator: renderer.Tint{FG: renderer.Colors{color.FgWhite}}, // White separators } table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewColorized(colorCfg)), tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{AutoWrap: tw.WrapNormal}, // Wrap long content Alignment: tw.CellAlignment{Global: tw.AlignLeft}, // Left-align rows ColMaxWidths: tw.CellWidth{Global: 25}, }, Footer: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignRight}, }, }), ) table.Header([]string{"ID", "Description", "Status"}) table.Bulk(data) table.Footer([]string{"", "Total", "3"}) table.Render() } ``` **Output** (colors visible in ANSI-compatible terminals): ![Colorized Table with Long Values](_readme/color_1.png "Title") #### 5. Streaming Table with Truncation Stream a table incrementally with truncation and a footer, simulating a real-time data feed (inspired by `TestOceanStreamTruncation` and `TestOceanStreamSlowOutput`). ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "log" "os" "time" ) func main() { table := tablewriter.NewTable(os.Stdout, tablewriter.WithStreaming(tw.StreamConfig{Enable: true})) // Start streaming if err := table.Start(); err != nil { log.Fatalf("Start failed: %v", err) } defer table.Close() // Stream header table.Header([]string{"ID", "Description", "Status"}) // Stream rows with simulated delay data := [][]string{ {"1", "This description is too long", "OK"}, {"2", "Short desc", "DONE"}, {"3", "Another long description here", "ERROR"}, } for _, row := range data { table.Append(row) time.Sleep(500 * time.Millisecond) // Simulate real-time data feed } // Stream footer table.Footer([]string{"", "Total", "3"}) } ``` **Output** (appears incrementally): ``` ┌────────┬───────────────┬──────────┐ │ ID │ DESCRIPTION │ STATUS │ ├────────┼───────────────┼──────────┤ │ 1 │ This │ OK │ │ │ description │ │ │ │ is too long │ │ │ 2 │ Short desc │ DONE │ │ 3 │ Another long │ ERROR │ │ │ description │ │ │ │ here │ │ ├────────┼───────────────┼──────────┤ │ │ Total │ 3 │ └────────┴───────────────┴──────────┘ ``` **Note**: Long descriptions are truncated with `…` due to fixed column widths. The output appears row-by-row, simulating a real-time feed. #### 6. Hierarchical Merging for Organizational Data Show hierarchical merging for a tree-like structure, such as an organizational hierarchy (inspired by `TestMergeHierarchicalUnicode`). ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { data := [][]string{ {"Engineering", "Backend", "API Team", "Alice"}, {"Engineering", "Backend", "Database Team", "Bob"}, {"Engineering", "Frontend", "UI Team", "Charlie"}, {"Marketing", "Digital", "SEO Team", "Dave"}, {"Marketing", "Digital", "Content Team", "Eve"}, } table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}}, })), tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, Row: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical}, Alignment: tw.CellAlignment{Global: tw.AlignLeft}, }, }), ) table.Header([]string{"Department", "Division", "Team", "Lead"}) table.Bulk(data) table.Render() } ``` **Output**: ``` ┌────────────┬──────────┬──────────────┬────────┐ │ DEPARTMENT │ DIVISION │ TEAM │ LEAD │ ├────────────┼──────────┼──────────────┼────────┤ │ Engineering│ Backend │ API Team │ Alice │ │ │ ├──────────────┼────────┤ │ │ │ Database Team│ Bob │ │ │ Frontend ├──────────────┼────────┤ │ │ │ UI Team │ Charlie│ ├────────────┼──────────┼──────────────┼────────┤ │ Marketing │ Digital │ SEO Team │ Dave │ │ │ ├──────────────┼────────┤ │ │ │ Content Team │ Eve │ └────────────┴──────────┴──────────────┴────────┘ ``` **Note**: Hierarchical merging groups repeated values (e.g., "Engineering" spans multiple rows, "Backend" spans two teams), creating a tree-like structure. #### 7. Custom Padding with Merging Showcase custom padding and combined horizontal/vertical merging (inspired by `TestMergeWithPadding` in `merge_test.go`). ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { data := [][]string{ {"1/1/2014", "Domain name", "Successful", "Successful"}, {"1/1/2014", "Domain name", "Pending", "Waiting"}, {"1/1/2014", "Domain name", "Successful", "Rejected"}, {"", "", "TOTAL", "$145.93"}, } table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}}, })), tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeBoth}, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}}, }, Footer: tw.CellConfig{ Padding: tw.CellPadding{ Global: tw.Padding{Left: "*", Right: "*"}, PerColumn: []tw.Padding{{}, {}, {Bottom: "^"}, {Bottom: "^"}}, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}}, }, }), ) table.Header([]string{"Date", "Description", "Status", "Conclusion"}) table.Bulk(data) table.Render() } ``` **Output**: ``` ┌──────────┬─────────────┬────────────┬────────────┐ │ DATE │ DESCRIPTION │ STATUS │ CONCLUSION │ ├──────────┼─────────────┼────────────┴────────────┤ │ 1/1/2014 │ Domain name │ Successful │ │ │ ├────────────┬────────────┤ │ │ │ Pending │ Waiting │ │ │ ├────────────┼────────────┤ │ │ │ Successful │ Rejected │ ├──────────┼─────────────┼────────────┼────────────┤ │ │ │ TOTAL │ $145.93 │ │ │ │^^^^^^^^^^^^│^^^^^^^^^^^^│ └──────────┴─────────────┴────────────┴────────────┘ ``` #### 8. Nested Tables Create a table with nested sub-tables for complex layouts (inspired by `TestMasterClass` in `extra_test.go`). ```go package main import ( "bytes" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { // Helper to create a sub-table createSubTable := func(s string) string { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Borders: tw.BorderNone, Symbols: tw.NewSymbols(tw.StyleASCII), Settings: tw.Settings{ Separators: tw.Separators{BetweenRows: tw.On}, Lines: tw.Lines{ShowFooterLine: tw.On}, }, })), tablewriter.WithConfig(tablewriter.Config{ MaxWidth: 10, Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, }), ) table.Append([]string{s, s}) table.Append([]string{s, s}) table.Render() return buf.String() } // Main table table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Borders: tw.BorderNone, Settings: tw.Settings{Separators: tw.Separators{BetweenColumns: tw.On}}, })), tablewriter.WithConfig(tablewriter.Config{ MaxWidth: 30, Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, }), ) table.Append([]string{createSubTable("A"), createSubTable("B")}) table.Append([]string{createSubTable("C"), createSubTable("D")}) table.Render() } ``` **Output**: ``` A | A │ B | B ---+--- │ ---+--- A | A │ B | B C | C │ D | D ---+--- │ ---+--- C | C │ D | D ``` #### 9. Structs with Database Render a table from a slice of structs, simulating a database query (inspired by `TestStructTableWithDB` in `struct_test.go`). ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) type Employee struct { ID int Name string Age int Department string Salary float64 } func employeeStringer(e interface{}) []string { emp, ok := e.(Employee) if !ok { return []string{"Error: Invalid type"} } return []string{ fmt.Sprintf("%d", emp.ID), emp.Name, fmt.Sprintf("%d", emp.Age), emp.Department, fmt.Sprintf("%.2f", emp.Salary), } } func main() { employees := []Employee{ {ID: 1, Name: "Alice Smith", Age: 28, Department: "Engineering", Salary: 75000.50}, {ID: 2, Name: "Bob Johnson", Age: 34, Department: "Marketing", Salary: 62000.00}, {ID: 3, Name: "Charlie Brown", Age: 45, Department: "HR", Salary: 80000.75}, } table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleRounded), })), tablewriter.WithStringer(employeeStringer), tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{AutoFormat: tw.On}, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }, Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}}, Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignRight}}, }), ) table.Header([]string{"ID", "Name", "Age", "Department", "Salary"}) for _, emp := range employees { table.Append(emp) } totalSalary := 0.0 for _, emp := range employees { totalSalary += emp.Salary } table.Footer([]string{"", "", "", "Total", fmt.Sprintf("%.2f", totalSalary)}) table.Render() } ``` **Output**: ``` ╭────┬───────────────┬─────┬─────────────┬───────────╮ │ ID │ NAME │ AGE │ DEPARTMENT │ SALARY │ ├────┼───────────────┼─────┼─────────────┼───────────┤ │ 1 │ Alice Smith │ 28 │ Engineering │ 75000.50 │ │ 2 │ Bob Johnson │ 34 │ Marketing │ 62000.00 │ │ 3 │ Charlie Brown │ 45 │ HR │ 80000.75 │ ├────┼───────────────┼─────┼─────────────┼───────────┤ │ │ │ │ Total │ 217001.25 │ ╰────┴───────────────┴─────┴─────────────┴───────────╯ ``` #### 10. Simple Html Table ```go package main import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { data := [][]string{ {"North", "Q1 & Q2", "Q1 & Q2", "$2200.00"}, {"South", "Q1", "Q1", "$1000.00"}, {"South", "Q2", "Q2", "$1200.00"}, } // Configure HTML with custom CSS classes and content escaping htmlCfg := renderer.HTMLConfig{ TableClass: "sales-table", HeaderClass: "table-header", BodyClass: "table-body", FooterClass: "table-footer", RowClass: "table-row", HeaderRowClass: "header-row", FooterRowClass: "footer-row", EscapeContent: true, // Escape HTML characters (e.g., "&" to "&") } table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewHTML(htmlCfg)), tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}, // Merge identical header cells Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}, // Merge identical row cells Alignment: tw.CellAlignment{Global: tw.AlignLeft}, }, Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignRight}}, }), ) table.Header([]string{"Region", "Quarter", "Quarter", "Sales"}) table.Bulk(data) table.Footer([]string{"", "", "Total", "$4400.00"}) table.Render() } ``` **Output**: ```
REGION QUARTER SALES
North Q1 & Q2 $2200.00
South Q1 $1000.00
South Q2 $1200.00
``` #### 11. SVG Support ```go package main import ( "fmt" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "os" ) type Age int func (a Age) String() string { return fmt.Sprintf("%d yrs", a) } func main() { data := [][]any{ {"Alice", Age(25), "New York"}, {"Bob", Age(30), "Boston"}, } file, err := os.OpenFile("out.svg", os.O_CREATE|os.O_WRONLY, 0644) if err != nil { ll.Fatal(err) } defer file.Close() table := tablewriter.NewTable(file, tablewriter.WithRenderer(renderer.NewSVG())) table.Header("Name", "Age", "City") table.Bulk(data) table.Render() } ``` ```go NAME AGE CITY Alice 25 yrs New York Bob 30 yrs Boston ``` #### 12 Simple Application ```go package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "io/fs" "os" "path/filepath" "strings" "time" ) const ( folder = "📁" file = "📄" baseDir = "../" indentStr = " " ) func main() { table := tablewriter.NewTable(os.Stdout, tablewriter.WithTrimSpace(tw.Off)) table.Header([]string{"Tree", "Size", "Permissions", "Modified"}) err := filepath.WalkDir(baseDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.Name() == "." || d.Name() == ".." { return nil } // Calculate relative path depth relPath, err := filepath.Rel(baseDir, path) if err != nil { return err } depth := 0 if relPath != "." { depth = len(strings.Split(relPath, string(filepath.Separator))) - 1 } indent := strings.Repeat(indentStr, depth) var name string if d.IsDir() { name = fmt.Sprintf("%s%s %s", indent, folder, d.Name()) } else { name = fmt.Sprintf("%s%s %s", indent, file, d.Name()) } info, err := d.Info() if err != nil { return err } table.Append([]string{ name, Size(info.Size()).String(), info.Mode().String(), Time(info.ModTime()).Format(), }) return nil }) if err != nil { fmt.Fprintf(os.Stdout, "Error: %v\n", err) return } table.Render() } const ( KB = 1024 MB = KB * 1024 GB = MB * 1024 TB = GB * 1024 ) type Size int64 func (s Size) String() string { switch { case s < KB: return fmt.Sprintf("%d B", s) case s < MB: return fmt.Sprintf("%.2f KB", float64(s)/KB) case s < GB: return fmt.Sprintf("%.2f MB", float64(s)/MB) case s < TB: return fmt.Sprintf("%.2f GB", float64(s)/GB) default: return fmt.Sprintf("%.2f TB", float64(s)/TB) } } type Time time.Time func (t Time) Format() string { now := time.Now() diff := now.Sub(time.Time(t)) if diff.Seconds() < 60 { return "just now" } else if diff.Minutes() < 60 { return fmt.Sprintf("%d minutes ago", int(diff.Minutes())) } else if diff.Hours() < 24 { return fmt.Sprintf("%d hours ago", int(diff.Hours())) } else if diff.Hours() < 24*7 { return fmt.Sprintf("%d days ago", int(diff.Hours()/24)) } else { return time.Time(t).Format("Jan 2, 2006") } } ``` ``` ┌──────────────────┬─────────┬─────────────┬──────────────┐ │ TREE │ SIZE │ PERMISSIONS │ MODIFIED │ ├──────────────────┼─────────┼─────────────┼──────────────┤ │ 📁 filetable │ 160 B │ drwxr-xr-x │ just now │ │ 📄 main.go │ 2.19 KB │ -rw-r--r-- │ 22 hours ago │ │ 📄 out.txt │ 0 B │ -rw-r--r-- │ just now │ │ 📁 testdata │ 128 B │ drwxr-xr-x │ 1 days ago │ │ 📄 a.txt │ 11 B │ -rw-r--r-- │ 1 days ago │ │ 📄 b.txt │ 17 B │ -rw-r--r-- │ 1 days ago │ │ 📁 symbols │ 128 B │ drwxr-xr-x │ just now │ │ 📄 main.go │ 4.58 KB │ -rw-r--r-- │ 1 hours ago │ │ 📄 out.txt │ 8.72 KB │ -rw-r--r-- │ just now │ └──────────────────┴─────────┴─────────────┴──────────────┘ ``` ## Changes - `AutoFormat` changes See [#261](https://github.com/olekukonko/tablewriter/issues/261) ## Command-Line Tool The `csv2table` tool converts CSV files to ASCII tables. See `cmd/csv2table/csv2table.go` for details. Example usage: ```bash csv2table -f test.csv -h true -a left ``` ## Contributing Contributions are welcome! Submit issues or pull requests to the [GitHub repository](https://github.com/olekukonko/tablewriter). ## License MIT License. See the [LICENSE](LICENSE) file for details.tablewriter-1.0.9/README_LEGACY.md000066400000000000000000000363101504170215200163500ustar00rootroot00000000000000ASCII Table Writer ========= [![ci](https://github.com/olekukonko/tablewriter/workflows/ci/badge.svg?branch=master)](https://github.com/olekukonko/tablewriter/actions?query=workflow%3Aci) [![Total views](https://img.shields.io/sourcegraph/rrc/github.com/olekukonko/tablewriter.svg)](https://sourcegraph.com/github.com/olekukonko/tablewriter) [![Godoc](https://godoc.org/github.com/olekukonko/tablewriter?status.svg)](https://godoc.org/github.com/olekukonko/tablewriter) ## Important Notice: Modernization in Progress The `tablewriter` package is being modernized on the `prototype` branch with generics, streaming support, and a modular design, targeting `v0.2.0`. Until this is released: **For Production Use**: Use the stable version `v0.0.5`: ```bash go get github.com/olekukonko/tablewriter@v0.0.5 ``` #### For Development Preview: Try the in-progress version (unstable) ```bash go get github.com/olekukonko/tablewriter@master ``` #### Features - Automatic Padding - Support Multiple Lines - Supports Alignment - Support Custom Separators - Automatic Alignment of numbers & percentage - Write directly to http , file etc via `io.Writer` - Read directly from CSV file - Optional row line via `SetRowLine` - Normalise table header - Make CSV Headers optional - Enable or disable table border - Set custom footer support - Optional identical cells merging - Set custom caption - Optional reflowing of paragraphs in multi-line cells. #### Example 1 - Basic ```go data := [][]string{ []string{"A", "The Good", "500"}, []string{"B", "The Very very Bad Man", "288"}, []string{"C", "The Ugly", "120"}, []string{"D", "The Gopher", "800"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Name", "Sign", "Rating"}) for _, v := range data { table.Append(v) } table.Render() // Send output ``` ##### Output 1 ``` +------+-----------------------+--------+ | NAME | SIGN | RATING | +------+-----------------------+--------+ | A | The Good | 500 | | B | The Very very Bad Man | 288 | | C | The Ugly | 120 | | D | The Gopher | 800 | +------+-----------------------+--------+ ``` #### Example 2 - Without Border / Footer / Bulk Append ```go data := [][]string{ []string{"1/1/2014", "Domain name", "2233", "$10.98"}, []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer table.EnableBorder(false) // Set Border to false table.AppendBulk(data) // Add Bulk Data table.Render() ``` ##### Output 2 ``` DATE | DESCRIPTION | CV2 | AMOUNT -----------+--------------------------+-------+---------- 1/1/2014 | Domain name | 2233 | $10.98 1/1/2014 | January Hosting | 2233 | $54.95 1/4/2014 | February Hosting | 2233 | $51.00 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 -----------+--------------------------+-------+---------- TOTAL | $146 93 --------+---------- ``` #### Example 3 - CSV ```go table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test_info.csv", true) table.SetAlignment(tablewriter.ALIGN_LEFT) // Set Alignment table.Render() ``` ##### Output 3 ``` +----------+--------------+------+-----+---------+----------------+ | FIELD | TYPE | NULL | KEY | DEFAULT | EXTRA | +----------+--------------+------+-----+---------+----------------+ | user_id | smallint(5) | NO | PRI | NULL | auto_increment | | username | varchar(10) | NO | | NULL | | | password | varchar(100) | NO | | NULL | | +----------+--------------+------+-----+---------+----------------+ ``` #### Example 4 - Custom Separator ```go table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test.csv", true) table.SetRowLine(true) // Enable row line // Change table lines table.SetCenterSeparator("*") table.SetColumnSeparator("╪") table.SetRowSeparator("-") table.SetAlignment(tablewriter.ALIGN_LEFT) table.Render() ``` ##### Output 4 ``` *------------*-----------*---------* ╪ FIRST NAME ╪ LAST NAME ╪ SSN ╪ *------------*-----------*---------* ╪ John ╪ Barry ╪ 123456 ╪ *------------*-----------*---------* ╪ Kathy ╪ Smith ╪ 687987 ╪ *------------*-----------*---------* ╪ Bob ╪ McCornick ╪ 3979870 ╪ *------------*-----------*---------* ``` #### Example 5 - Markdown Format ```go data := [][]string{ []string{"1/1/2014", "Domain name", "2233", "$10.98"}, []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) table.SetCenterSeparator("|") table.AppendBulk(data) // Add Bulk Data table.Render() ``` ##### Output 5 ``` | DATE | DESCRIPTION | CV2 | AMOUNT | |----------|--------------------------|------|--------| | 1/1/2014 | Domain name | 2233 | $10.98 | | 1/1/2014 | January Hosting | 2233 | $54.95 | | 1/4/2014 | February Hosting | 2233 | $51.00 | | 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 | ``` #### Example 6 - Identical cells merging ```go data := [][]string{ []string{"1/1/2014", "Domain name", "1234", "$10.98"}, []string{"1/1/2014", "January Hosting", "2345", "$54.95"}, []string{"1/4/2014", "February Hosting", "3456", "$51.00"}, []string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) table.SetFooter([]string{"", "", "Total", "$146.93"}) table.SetAutoMergeCells(true) table.SetRowLine(true) table.AppendBulk(data) table.Render() ``` ##### Output 6 ``` +----------+--------------------------+-------+---------+ | DATE | DESCRIPTION | CV2 | AMOUNT | +----------+--------------------------+-------+---------+ | 1/1/2014 | Domain name | 1234 | $10.98 | + +--------------------------+-------+---------+ | | January Hosting | 2345 | $54.95 | +----------+--------------------------+-------+---------+ | 1/4/2014 | February Hosting | 3456 | $51.00 | + +--------------------------+-------+---------+ | | February Extra Bandwidth | 4567 | $30.00 | +----------+--------------------------+-------+---------+ | TOTAL | $146 93 | +----------+--------------------------+-------+---------+ ``` #### Example 7 - Identical cells merging (specify the column index to merge) ```go data := [][]string{ []string{"1/1/2014", "Domain name", "1234", "$10.98"}, []string{"1/1/2014", "January Hosting", "1234", "$10.98"}, []string{"1/4/2014", "February Hosting", "3456", "$51.00"}, []string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) table.SetFooter([]string{"", "", "Total", "$146.93"}) table.SetAutoMergeCellsByColumnIndex([]int{2, 3}) table.SetRowLine(true) table.AppendBulk(data) table.Render() ``` ##### Output 7 ``` +----------+--------------------------+-------+---------+ | DATE | DESCRIPTION | CV2 | AMOUNT | +----------+--------------------------+-------+---------+ | 1/1/2014 | Domain name | 1234 | $10.98 | +----------+--------------------------+ + + | 1/1/2014 | January Hosting | | | +----------+--------------------------+-------+---------+ | 1/4/2014 | February Hosting | 3456 | $51.00 | +----------+--------------------------+-------+---------+ | 1/4/2014 | February Extra Bandwidth | 4567 | $30.00 | +----------+--------------------------+-------+---------+ | TOTAL | $146.93 | +----------+--------------------------+-------+---------+ ``` #### Table with color ```go data := [][]string{ []string{"1/1/2014", "Domain name", "2233", "$10.98"}, []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer table.EnableBorder(false) // Set Border to false table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor}, tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor}, tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor}, tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor}) table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}) table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{}, tablewriter.Colors{tablewriter.Bold}, tablewriter.Colors{tablewriter.FgHiRedColor}) table.AppendBulk(data) table.Render() ``` #### Table with color Output ![Table with Color](https://cloud.githubusercontent.com/assets/6460392/21101956/bbc7b356-c0a1-11e6-9f36-dba694746efc.png) #### Example - 8 Table Cells with Color Individual Cell Colors from `func Rich` take precedence over Column Colors ```go data := [][]string{ []string{"Test1Merge", "HelloCol2 - 1", "HelloCol3 - 1", "HelloCol4 - 1"}, []string{"Test1Merge", "HelloCol2 - 2", "HelloCol3 - 2", "HelloCol4 - 2"}, []string{"Test1Merge", "HelloCol2 - 3", "HelloCol3 - 3", "HelloCol4 - 3"}, []string{"Test2Merge", "HelloCol2 - 4", "HelloCol3 - 4", "HelloCol4 - 4"}, []string{"Test2Merge", "HelloCol2 - 5", "HelloCol3 - 5", "HelloCol4 - 5"}, []string{"Test2Merge", "HelloCol2 - 6", "HelloCol3 - 6", "HelloCol4 - 6"}, []string{"Test2Merge", "HelloCol2 - 7", "HelloCol3 - 7", "HelloCol4 - 7"}, []string{"Test3Merge", "HelloCol2 - 8", "HelloCol3 - 8", "HelloCol4 - 8"}, []string{"Test3Merge", "HelloCol2 - 9", "HelloCol3 - 9", "HelloCol4 - 9"}, []string{"Test3Merge", "HelloCol2 - 10", "HelloCol3 -10", "HelloCol4 - 10"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Col1", "Col2", "Col3", "Col4"}) table.SetFooter([]string{"", "", "Footer3", "Footer4"}) table.EnableBorder(false) table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor}, tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor}, tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor}, tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor}) table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}) table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{}, tablewriter.Colors{tablewriter.Bold}, tablewriter.Colors{tablewriter.FgHiRedColor}) colorData1 := []string{"TestCOLOR1Merge", "HelloCol2 - COLOR1", "HelloCol3 - COLOR1", "HelloCol4 - COLOR1"} colorData2 := []string{"TestCOLOR2Merge", "HelloCol2 - COLOR2", "HelloCol3 - COLOR2", "HelloCol4 - COLOR2"} for i, row := range data { if i == 4 { table.Rich(colorData1, []tablewriter.Colors{tablewriter.Colors{}, tablewriter.Colors{tablewriter.Normal, tablewriter.FgCyanColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgWhiteColor}, tablewriter.Colors{}}) table.Rich(colorData2, []tablewriter.Colors{tablewriter.Colors{tablewriter.Normal, tablewriter.FgMagentaColor}, tablewriter.Colors{}, tablewriter.Colors{tablewriter.Bold, tablewriter.BgRedColor}, tablewriter.Colors{tablewriter.FgHiGreenColor, tablewriter.Italic, tablewriter.BgHiCyanColor}}) } table.Append(row) } table.SetAutoMergeCells(true) table.Render() ``` ##### Table cells with color Output ![Table cells with Color](https://user-images.githubusercontent.com/9064687/63969376-bcd88d80-ca6f-11e9-9466-c3d954700b25.png) #### Example 9 - Set table caption ```go data := [][]string{ []string{"A", "The Good", "500"}, []string{"B", "The Very very Bad Man", "288"}, []string{"C", "The Ugly", "120"}, []string{"D", "The Gopher", "800"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Name", "Sign", "Rating"}) table.SetCaption(true, "Movie ratings.") for _, v := range data { table.Append(v) } table.Render() // Send output ``` Note: Caption text will wrap with total width of rendered table. ##### Output 9 ``` +------+-----------------------+--------+ | NAME | SIGN | RATING | +------+-----------------------+--------+ | A | The Good | 500 | | B | The Very very Bad Man | 288 | | C | The Ugly | 120 | | D | The Gopher | 800 | +------+-----------------------+--------+ Movie ratings. ``` #### Example 10 - Set NoWhiteSpace and TablePadding option ```go data := [][]string{ {"node1.example.com", "Ready", "compute", "1.11"}, {"node2.example.com", "Ready", "compute", "1.11"}, {"node3.example.com", "Ready", "compute", "1.11"}, {"node4.example.com", "NotReady", "compute", "1.11"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Name", "Status", "Role", "Version"}) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.EnableBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) table.AppendBulk(data) // Add Bulk Data table.Render() ``` ##### Output 10 ``` NAME STATUS ROLE VERSION node1.example.com Ready compute 1.11 node2.example.com Ready compute 1.11 node3.example.com Ready compute 1.11 node4.example.com NotReady compute 1.11 ``` #### Render table into a string Instead of rendering the table to `io.Stdout` you can also render it into a string. Go 1.10 introduced the `strings.Builder` type which implements the `io.Writer` interface and can therefore be used for this task. Example: ```go package main import ( "strings" "fmt" "github.com/olekukonko/tablewriter" ) func main() { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) /* * Code to fill the table */ table.Render() fmt.Println(tableString.String()) } ``` #### TODO - ~~Import Directly from CSV~~ - `done` - ~~Support for `SetFooter`~~ - `done` - ~~Support for `SetBorder`~~ - `done` - ~~Support table with uneven rows~~ - `done` - ~~Support custom alignment~~ - General Improvement & Optimisation - `NewHTML` Parse table from HTML tablewriter-1.0.9/_example/000077500000000000000000000000001504170215200156345ustar00rootroot00000000000000tablewriter-1.0.9/_example/filetable/000077500000000000000000000000001504170215200175635ustar00rootroot00000000000000tablewriter-1.0.9/_example/filetable/main.go000066400000000000000000000043001504170215200210330ustar00rootroot00000000000000package main import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" "io/fs" "os" "path/filepath" "strings" "time" ) const ( folder = "📁" file = "📄" baseDir = "../" indentStr = " " ) func main() { table := tablewriter.NewTable(os.Stdout, tablewriter.WithTrimSpace(tw.Off)) table.Header([]string{"Tree", "Size", "Permissions", "Modified"}) err := filepath.WalkDir(baseDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.Name() == "." || d.Name() == ".." { return nil } // Calculate relative path depth relPath, err := filepath.Rel(baseDir, path) if err != nil { return err } depth := 0 if relPath != "." { depth = len(strings.Split(relPath, string(filepath.Separator))) - 1 } indent := strings.Repeat(indentStr, depth) var name string if d.IsDir() { name = fmt.Sprintf("%s%s %s", indent, folder, d.Name()) } else { name = fmt.Sprintf("%s%s %s", indent, file, d.Name()) } info, err := d.Info() if err != nil { return err } table.Append([]string{ name, Size(info.Size()).String(), info.Mode().String(), Time(info.ModTime()).Format(), }) return nil }) if err != nil { fmt.Fprintf(os.Stdout, "Error: %v\n", err) return } table.Render() } const ( KB = 1024 MB = KB * 1024 GB = MB * 1024 TB = GB * 1024 ) type Size int64 func (s Size) String() string { switch { case s < KB: return fmt.Sprintf("%d B", s) case s < MB: return fmt.Sprintf("%.2f KB", float64(s)/KB) case s < GB: return fmt.Sprintf("%.2f MB", float64(s)/MB) case s < TB: return fmt.Sprintf("%.2f GB", float64(s)/GB) default: return fmt.Sprintf("%.2f TB", float64(s)/TB) } } type Time time.Time func (t Time) Format() string { now := time.Now() diff := now.Sub(time.Time(t)) if diff.Seconds() < 60 { return "just now" } else if diff.Minutes() < 60 { return fmt.Sprintf("%d minutes ago", int(diff.Minutes())) } else if diff.Hours() < 24 { return fmt.Sprintf("%d hours ago", int(diff.Hours())) } else if diff.Hours() < 24*7 { return fmt.Sprintf("%d days ago", int(diff.Hours()/24)) } else { return time.Time(t).Format("Jan 2, 2006") } } tablewriter-1.0.9/_example/filetable/out.txt000066400000000000000000000023441504170215200211360ustar00rootroot00000000000000┌──────────────────┬─────────┬─────────────┬──────────────┐ │ TREE │ SIZE │ PERMISSIONS │ MODIFIED │ ├──────────────────┼─────────┼─────────────┼──────────────┤ │ 📁 filetable │ 160 B │ drwxr-xr-x │ just now │ │ 📄 main.go │ 2.19 KB │ -rw-r--r-- │ 22 hours ago │ │ 📄 out.txt │ 0 B │ -rw-r--r-- │ just now │ │ 📁 testdata │ 128 B │ drwxr-xr-x │ 1 days ago │ │ 📄 a.txt │ 11 B │ -rw-r--r-- │ 1 days ago │ │ 📄 b.txt │ 17 B │ -rw-r--r-- │ 1 days ago │ │ 📁 symbols │ 128 B │ drwxr-xr-x │ just now │ │ 📄 main.go │ 4.58 KB │ -rw-r--r-- │ 1 hours ago │ │ 📄 out.txt │ 8.72 KB │ -rw-r--r-- │ just now │ └──────────────────┴─────────┴─────────────┴──────────────┘ tablewriter-1.0.9/_example/symbols/000077500000000000000000000000001504170215200173245ustar00rootroot00000000000000tablewriter-1.0.9/_example/symbols/main.go000066400000000000000000000111161504170215200205770ustar00rootroot00000000000000package main import ( "fmt" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "os" ) func main() { data := [][]string{ {"Engineering", "Backend", "API Team", "Alice"}, {"Engineering", "Backend", "Database Team", "Bob"}, {"Engineering", "Frontend", "UI Team", "Charlie"}, {"Marketing", "Digital", "SEO Team", "Dave"}, {"Marketing", "Digital", "Content Team", "Eve"}, } cnf := tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{Alignment: tw.AlignCenter}, }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHierarchical, Alignment: tw.AlignLeft, }, }, Debug: false, } // Create a custom border style DottedStyle := []tw.Symbols{ tw.NewSymbolCustom("Dotted"). WithRow("·"). WithColumn(":"). WithTopLeft("."). WithTopMid("·"). WithTopRight("."). WithMidLeft(":"). WithCenter("+"). WithMidRight(":"). WithBottomLeft("'"). WithBottomMid("·"). WithBottomRight("'"), // arrow style tw.NewSymbolCustom("Arrow"). WithRow("→"). WithColumn("↓"). WithTopLeft("↗"). WithTopMid("↑"). WithTopRight("↖"). WithMidLeft("→"). WithCenter("↔"). WithMidRight("←"). WithBottomLeft("↘"). WithBottomMid("↓"). WithBottomRight("↙"), // start style tw.NewSymbolCustom("Starry"). WithRow("★"). WithColumn("☆"). WithTopLeft("✧"). WithTopMid("✯"). WithTopRight("✧"). WithMidLeft("✦"). WithCenter("✶"). WithMidRight("✦"). WithBottomLeft("✧"). WithBottomMid("✯"). WithBottomRight("✧"), tw.NewSymbolCustom("Hearts"). WithRow("♥"). WithColumn("❤"). WithTopLeft("❥"). WithTopMid("♡"). WithTopRight("❥"). WithMidLeft("❣"). WithCenter("✚"). WithMidRight("❣"). WithBottomLeft("❦"). WithBottomMid("♡"). WithBottomRight("❦"), tw.NewSymbolCustom("Tech"). WithRow("="). WithColumn("||"). WithTopLeft("/*"). WithTopMid("##"). WithTopRight("*/"). WithMidLeft("//"). WithCenter("<>"). WithMidRight("\\"). WithBottomLeft("\\*"). WithBottomMid("##"). WithBottomRight("*/"), tw.NewSymbolCustom("Nature"). WithRow("~"). WithColumn("|"). WithTopLeft("🌱"). WithTopMid("🌿"). WithTopRight("🌱"). WithMidLeft("🍃"). WithCenter("❀"). WithMidRight("🍃"). WithBottomLeft("🌻"). WithBottomMid("🌾"). WithBottomRight("🌻"), tw.NewSymbolCustom("Artistic"). WithRow("▬"). WithColumn("▐"). WithTopLeft("◈"). WithTopMid("◊"). WithTopRight("◈"). WithMidLeft("◀"). WithCenter("⬔"). WithMidRight("▶"). WithBottomLeft("◭"). WithBottomMid("▣"). WithBottomRight("◮"), tw.NewSymbolCustom("8-Bit"). WithRow("■"). WithColumn("█"). WithTopLeft("╔"). WithTopMid("▲"). WithTopRight("╗"). WithMidLeft("◄"). WithCenter("♦"). WithMidRight("►"). WithBottomLeft("╚"). WithBottomMid("▼"). WithBottomRight("╝"), tw.NewSymbolCustom("Chaos"). WithRow("≈"). WithColumn("§"). WithTopLeft("⌘"). WithTopMid("∞"). WithTopRight("⌥"). WithMidLeft("⚡"). WithCenter("☯"). WithMidRight("♞"). WithBottomLeft("⌂"). WithBottomMid("∆"). WithBottomRight("◊"), tw.NewSymbolCustom("Dots"). WithRow("·"). WithColumn(" "). // Invisible column lines WithTopLeft("·"). WithTopMid("·"). WithTopRight("·"). WithMidLeft(" "). WithCenter("·"). WithMidRight(" "). WithBottomLeft("·"). WithBottomMid("·"). WithBottomRight("·"), tw.NewSymbolCustom("Blocks"). WithRow("▀"). WithColumn("█"). WithTopLeft("▛"). WithTopMid("▀"). WithTopRight("▜"). WithMidLeft("▌"). WithCenter("█"). WithMidRight("▐"). WithBottomLeft("▙"). WithBottomMid("▄"). WithBottomRight("▟"), tw.NewSymbolCustom("Zen"). WithRow("~"). WithColumn(" "). WithTopLeft(" "). WithTopMid("♨"). WithTopRight(" "). WithMidLeft(" "). WithCenter("☯"). WithMidRight(" "). WithBottomLeft(" "). WithBottomMid("♨"). WithBottomRight(" "), } var table *tablewriter.Table for _, style := range DottedStyle { ll.Info(style.Name() + " style") table = tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: style})), tablewriter.WithConfig(cnf), ) table.Header([]string{"Department", "Division", "Team", "Lead"}) table.Bulk(data) table.Render() fmt.Println() } } tablewriter-1.0.9/_example/symbols/out.txt000066400000000000000000000213311504170215200206740ustar00rootroot00000000000000INFO: Dotted style .··················································. : DEPARTMENT : DIVISION : TEAM : LEAD : :·············+··········+···············+·········: : Engineering : Backend : API Team : Alice : : : : Database Team : Bob : : : Frontend : UI Team : Charlie : : Marketing : Digital : SEO Team : Dave : : : : Content Team : Eve : '··················································' INFO: Arrow style ↗→→→→→→→→→→→→→↑→→→→→→→→→→↑→→→→→→→→→→→→→→→↑→→→→→→→→→↖ ↓ DEPARTMENT ↓ DIVISION ↓ TEAM ↓ LEAD ↓ →→→→→→→→→→→→→→↔→→→→→→→→→→↔→→→→→→→→→→→→→→→↔→→→→→→→→→← ↓ Engineering ↓ Backend ↓ API Team ↓ Alice ↓ ↓ ↓ ↓ Database Team ↓ Bob ↓ ↓ ↓ Frontend ↓ UI Team ↓ Charlie ↓ ↓ Marketing ↓ Digital ↓ SEO Team ↓ Dave ↓ ↓ ↓ ↓ Content Team ↓ Eve ↓ ↘→→→→→→→→→→→→→↓→→→→→→→→→→↓→→→→→→→→→→→→→→→↓→→→→→→→→→↙ INFO: Starry style ✧★★★★★★★★★★★★★✯★★★★★★★★★★✯★★★★★★★★★★★★★★★✯★★★★★★★★★✧ ☆ DEPARTMENT ☆ DIVISION ☆ TEAM ☆ LEAD ☆ ✦★★★★★★★★★★★★★✶★★★★★★★★★★✶★★★★★★★★★★★★★★★✶★★★★★★★★★✦ ☆ Engineering ☆ Backend ☆ API Team ☆ Alice ☆ ☆ ☆ ☆ Database Team ☆ Bob ☆ ☆ ☆ Frontend ☆ UI Team ☆ Charlie ☆ ☆ Marketing ☆ Digital ☆ SEO Team ☆ Dave ☆ ☆ ☆ ☆ Content Team ☆ Eve ☆ ✧★★★★★★★★★★★★★✯★★★★★★★★★★✯★★★★★★★★★★★★★★★✯★★★★★★★★★✧ INFO: Hearts style ❥♥♥♥♥♥♥♥♥♥♥♥♥♥♡♥♥♥♥♥♥♥♥♥♥♡♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♡♥♥♥♥♥♥♥♥♥❥ ❤ DEPARTMENT ❤ DIVISION ❤ TEAM ❤ LEAD ❤ ❣♥♥♥♥♥♥♥♥♥♥♥♥♥✚♥♥♥♥♥♥♥♥♥♥✚♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥✚♥♥♥♥♥♥♥♥♥❣ ❤ Engineering ❤ Backend ❤ API Team ❤ Alice ❤ ❤ ❤ ❤ Database Team ❤ Bob ❤ ❤ ❤ Frontend ❤ UI Team ❤ Charlie ❤ ❤ Marketing ❤ Digital ❤ SEO Team ❤ Dave ❤ ❤ ❤ ❤ Content Team ❤ Eve ❤ ❦♥♥♥♥♥♥♥♥♥♥♥♥♥♡♥♥♥♥♥♥♥♥♥♥♡♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♡♥♥♥♥♥♥♥♥♥❦ INFO: Tech style /*=============##==========##===============##=========*/ || DEPARTMENT || DIVISION || TEAM || LEAD || //=============<>==========<>===============<>==========\ || Engineering || Backend || API Team || Alice || || || || Database Team || Bob || || || Frontend || UI Team || Charlie || || Marketing || Digital || SEO Team || Dave || || || || Content Team || Eve || \*=============##==========##===============##=========*/ INFO: Nature style 🌱~~~~~~~~~~~🌿~~~~~~~~~🌿~~~~~~~~~~~~~~🌿~~~~~~~~🌱 | DEPARTMENT | DIVISION | TEAM | LEAD | 🍃~~~~~~~~~~~~❀~~~~~~~~~~❀~~~~~~~~~~~~~~~❀~~~~~~~~🍃 | Engineering | Backend | API Team | Alice | | | | Database Team | Bob | | | Frontend | UI Team | Charlie | | Marketing | Digital | SEO Team | Dave | | | | Content Team | Eve | 🌻~~~~~~~~~~~🌾~~~~~~~~~🌾~~~~~~~~~~~~~~🌾~~~~~~~~🌻 INFO: Artistic style ◈▬▬▬▬▬▬▬▬▬▬▬▬▬◊▬▬▬▬▬▬▬▬▬▬◊▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬◊▬▬▬▬▬▬▬▬▬◈ ▐ DEPARTMENT ▐ DIVISION ▐ TEAM ▐ LEAD ▐ ◀▬▬▬▬▬▬▬▬▬▬▬▬▬⬔▬▬▬▬▬▬▬▬▬▬⬔▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬⬔▬▬▬▬▬▬▬▬▬▶ ▐ Engineering ▐ Backend ▐ API Team ▐ Alice ▐ ▐ ▐ ▐ Database Team ▐ Bob ▐ ▐ ▐ Frontend ▐ UI Team ▐ Charlie ▐ ▐ Marketing ▐ Digital ▐ SEO Team ▐ Dave ▐ ▐ ▐ ▐ Content Team ▐ Eve ▐ ◭▬▬▬▬▬▬▬▬▬▬▬▬▬▣▬▬▬▬▬▬▬▬▬▬▣▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▣▬▬▬▬▬▬▬▬▬◮ INFO: 8-Bit style ╔■■■■■■■■■■■■■▲■■■■■■■■■■▲■■■■■■■■■■■■■■■▲■■■■■■■■■╗ █ DEPARTMENT █ DIVISION █ TEAM █ LEAD █ ◄■■■■■■■■■■■■■♦■■■■■■■■■■♦■■■■■■■■■■■■■■■♦■■■■■■■■■► █ Engineering █ Backend █ API Team █ Alice █ █ █ █ Database Team █ Bob █ █ █ Frontend █ UI Team █ Charlie █ █ Marketing █ Digital █ SEO Team █ Dave █ █ █ █ Content Team █ Eve █ ╚■■■■■■■■■■■■■▼■■■■■■■■■■▼■■■■■■■■■■■■■■■▼■■■■■■■■■╝ INFO: Chaos style ⌘≈≈≈≈≈≈≈≈≈≈≈≈≈∞≈≈≈≈≈≈≈≈≈≈∞≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈∞≈≈≈≈≈≈≈≈≈⌥ § DEPARTMENT § DIVISION § TEAM § LEAD § ⚡≈≈≈≈≈≈≈≈≈≈≈≈☯≈≈≈≈≈≈≈≈≈≈☯≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈☯≈≈≈≈≈≈≈≈≈♞ § Engineering § Backend § API Team § Alice § § § § Database Team § Bob § § § Frontend § UI Team § Charlie § § Marketing § Digital § SEO Team § Dave § § § § Content Team § Eve § ⌂≈≈≈≈≈≈≈≈≈≈≈≈≈∆≈≈≈≈≈≈≈≈≈≈∆≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈∆≈≈≈≈≈≈≈≈≈◊ INFO: Dots style ···················································· DEPARTMENT DIVISION TEAM LEAD ·················································· Engineering Backend API Team Alice Database Team Bob Frontend UI Team Charlie Marketing Digital SEO Team Dave Content Team Eve ···················································· INFO: Blocks style ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ █ DEPARTMENT █ DIVISION █ TEAM █ LEAD █ ▌▀▀▀▀▀▀▀▀▀▀▀▀▀█▀▀▀▀▀▀▀▀▀▀█▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀█▀▀▀▀▀▀▀▀▀▐ █ Engineering █ Backend █ API Team █ Alice █ █ █ █ Database Team █ Bob █ █ █ Frontend █ UI Team █ Charlie █ █ Marketing █ Digital █ SEO Team █ Dave █ █ █ █ Content Team █ Eve █ ▙▀▀▀▀▀▀▀▀▀▀▀▀▀▄▀▀▀▀▀▀▀▀▀▀▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄▀▀▀▀▀▀▀▀▀▟ INFO: Zen style ~~~~~~~~~~~~~♨~~~~~~~~~~♨~~~~~~~~~~~~~~~♨~~~~~~~~~ DEPARTMENT DIVISION TEAM LEAD ~~~~~~~~~~~~~☯~~~~~~~~~~☯~~~~~~~~~~~~~~~☯~~~~~~~~~ Engineering Backend API Team Alice Database Team Bob Frontend UI Team Charlie Marketing Digital SEO Team Dave Content Team Eve ~~~~~~~~~~~~~♨~~~~~~~~~~♨~~~~~~~~~~~~~~~♨~~~~~~~~~ tablewriter-1.0.9/_readme/000077500000000000000000000000001504170215200154365ustar00rootroot00000000000000tablewriter-1.0.9/_readme/color_1.png000066400000000000000000005140041504170215200175060ustar00rootroot00000000000000PNG  IHDR KiCCPICC ProfileHWXS[R!D@JM@J-"JHcBP ]D aWZ *+b tW77wϙ9w;Ri @$O"uh0_ r,˻QZEK( @ NoHey7'UȠW)q 7)q _鳉B:/@|AԡhD(@Ond!s!6pNRN45Ari]rssXê) Q ${rCA 6(.+13SGmr.`BV. ߙ+;)Eyq!\aO>a,XH‰Dž"b"I|y14'+y3cTEҼ8xy?4J.,5 LY@UT=Ad C?30"G @>axS]@zR%<8xS z@F ` 9*=?~g8 g3@b1D p W?Xq61w{SBpgP6˱'VPǽ:Tƙp]< Ynʬh-PʼnRQ(6CGji(sc~T ;3t~6l%;Nb&XւUo O6f?Ye&N5NN_T}yiyȝ,.gd8!b$,g'gWMtwa|#߹s96p@!WqBo:}3p^P @τ\`(%`9X&T`?M$8 .+ WOx;ABC>bX"3F|@$AT$ d&2)AV"هFN"6D^#P UGuP# Qơ t Z.@eh% COh;11Scc\,K16+JJk֎uaq"Y\!x<.%x9^kCF O0AJ("AxG$Dk;܋I,  b1D"IޤHG*"#"']%u>&dgr9,!K;Wȟ)K'%"L,l4R.S:(ZTk75EG-RPQߨyE檕U;P:W=E]T} ;oh4͏Lˣ-UN>h045xB9uW5^)tK:>^@/_wiR44|ٚ5oih1FiEjj-کuA6IJ;P[@{) a2m3O'KDgNNn4 ݣLi1s˘7  [[ W ;Eޢg+ӟgxg,sWY!YgGfIٓKM=,іdKNO6^5vUjoLZsԥtZZu떯RY~¿bz׿ puMFJ6},|{KJҭĭ[nKv/ lC*t{uNÝjEM箔]Wvnuݲd/ثǾ}7o>>P{CCuHáq<ɴeǨ=^pDɌ'5=5ѧ[τ96ss{oyEKnZ\[Vֺx\ilvՓλ~Fč7oJ~[x;~ݹk/}`w}Q죻_w6/WKWW枨r}~_ACGs?=< KWۯ¾e_ (6- <7Rǫ·}QiOXu+n.ws hyt:p;w*  6GMMΤ?=JU0w߃?SbeXIfMM*>F(iNxASCIIScreenshot{4 pHYs%%IR$iTXtXML:com.adobe.xmp 390 504 Screenshot oiDOT(Eb>@IDATx}fEyܺ6zYzUAY0.ƨ4 cĚ v) cݥcD)Kۨ[?9{wf~3ۧ3眶vM І\f@ӽ,s˜P (mmǂOQ\gF --{[h?p<Ÿg@ShdDt/A[ jÐA& tyAyFMdR z,LcnjUV*V8pkrAN"HF֌*xvF|>YF*wCúQPaK,I===^m#X4/GjY-`!Qs^*P-I1%VS_N1vDίOrGrH5rMib I̟H% 0 Ә1%gJffei#PR^aR5>*+I"O4]0О"-e{J~o9&M-&wWw'wV &TMǥV i.c촜sxiɗ']hŀqˠ&j[TS-3X5cʏ ]q?i #-4 b' 5E稞\x3l:dJP p)_|4g˓߻zjk{V#GJ,}%V*VZT=$#.cdU dtIѨM>}ҀPN>O%̉^l-i*-A0B>VτK0Za+E ёĎ(Ҋ|92D | 2Y! :q2WF($a]GR}vx_wwKF@hzO9c[>72 ۈآ/ U8 J:^e>,U8@R$2< JZهR?|xYVS{ԈAu)8:a/EP!2%I"<26Wx~3bbl&n{(3dԋ I݃앾E_.I~d)DdyBt`E|jhARY:[T=QX{,=>V<`%κ+x#cDvS@Ygz%>40ieq[":MK~~g}x/Ey%b.n'l6T[.!4$h<4 ~O^?9#k6K3?l2.#S4u!e" Jjqv( 2HvV2<1bx>bmysآ >S$ )'V2͉jW3 [o?4 ŢZiNY4t M0$+,2a`)[Z!"W?T`߳0ҩ!;>&_zmW}ů̡mL l6bj]{<[Wx&!{vu,S O HFxD ٖ[CGӭCsە}lC:1G?!S7'^nx1LX2t!;?͚((?d?cekXBi+.)z=x4bؑr fq.Sq$Z:,CQ2=O ޕʺf@đ,kTS0D=xpd#$gIzY*f0wW:6|ڧt`}|Սamj<vr$ʫJYXIK>|TVCWO\r0y41c4 jEM:g|2;t|D% |(aNeƑ2L'|@.F=F;ۯtaC6KH())1'#Q ɵ,fdeZ鿖[t{UFL1c0U0)b٘eX?nCvŋn7~] ŋKpK8q=,d1 < ̷P z1寉-zoY^İ;ύCM=)W*k0f!2]hcR" ʿQkLe6+EÊ6UU 5cAbK QBp:e!Ϝb+J(go!; !"*ҒXyIaACeꆰwgGA/ZwMv函_L^K"f-W' .BwW|+4d] #kK8L4jpC7%2pɭ7u?^FK%RagƁ; eÆC'IG]JXG/eWo2wI6[P$4[8X~@Sc{v_oiF:5nԗM舢c*5Wrߕ@Uvu=zVbŲCzG,a!"N=ć^ } eZgPvi=E=RZ4Aj*?JE&6̀?n dv'8a-f#53I/a4W%]U_\4KZ1إ1h`m~1}(Q|Lc7[r˭7'/̈́) .ڿjpdhJK~mQB+e5P1| /P~< U@$rc!]t:E/d hZ[GhVk?qi'jt dͥ^Q XW Ύt՗e5ی5.8wRX z 6\evQ6" âv^*$Vv GJ+>j̚_mlO@>Ɨ; #r޴t щKuضR_amg;П<凩rӦ&FX#_okVu(korٽrԙE?`a?`ojmxPg`-IN_ COvS^jJ *`x}Ch|;]_ܱCW{ge>$?"Li/=bҵcI|֚t˗-cuj]l+?6*}&q 㡩jnf?wXϋKuk= HPP_tF'kGw3V3f[19L" K@,M+T Ye x5g6 b b%][_#_%66o(>ٔowvu٨/>}& nWm1Y[qĒS^2; qx5:&K]cU Cb,3T.e]+' `0f~^E=TZ0,mXO\7v2K /yUӝv4ko~6.Ƞガ ȷ ff\Y+Dlc=3ĊV_MDs\$ZVg?Q,Km CFřp2gvA3l &w oiyqD983M".ߡ? o\sh 's?8s3 l&~liYf~ q( pgE_T< 1@Of )FRa`(?$ vA!GW𙧛B׳^2Yܢ-߶4ti)+x=@5^tB&j^ೖ8DWI9DJ@j#[o5p۷GDY&ʰw z oNa5sKẫE=<Ĩfiʐ)u+e |tsǙd'"aizoHm)Q}xwyPR ě, ٰn|Ĩw)пpX=EId5Pa?"yl49g NeWBBfXsY֟dG+RƆZ[޸賬At89[yU*FefLcoŖi[W+ygrRÏ:џ9 X~vͥAy_ٷ&/Dʸ Q4U9'[NQ6#o 5a2,Y4d yG)b`1^MlxHȪOy u|fekքQx(g]y$&'i\;Vbʧ+7 2܃SPȯe!P+e%\Q|>t-X:7 L-s1\~)B$!%;2ˇ(@Z$Y4Xspoj[2|ۿO1ǢE\"!*w2zb3RxO & -ߕX&'!O5쎸0J'?#cSOzAj|>8'V)z1<}X[BG?cO<%3-w^E]VA"A9xLMh&7ЁR"ϙ^yiETeL{j'7'DxV)"&ku{\K3ha<@ 0\QOo4V4-^|.z>6!7P6 i\0'q 54o/_zW *]2Ow0g)Cȍxm-zSEAX_7V~g]@@饵] W^ŧq7 ||Mk Pɬ^cA߼}m րK>՟7sqIYowciKF>~iȾlR?RwQ͋ !* *Ӹaxc0y^ 9aBTI$ɡgA's [&Y~Mn2*c"iL2~X\X)/v/V`P خc('HhN^[qДqI\‹t no!"lZN ƄLR_K;`i7j/*^h/ڸ7ƀvX>mAT,t=?n|쮼Fڈ;7g+WFTw!L&p0FI!P QF80b1 lPk!8B=#9 4 3Hb3Z;=| Ԁ$`'  'xXR&2fU'"h)^bc(i5~2Fm~q4EY_02KH+-H1,'=I˧DdAʑHhCd`D! 2ŇȲȗ υjI&_$|1#~=^U|"^*̙<._!lᆪ˰_`&`crj4JPT6Sʔuc|ᩅMȓ0@)(f'vho8]|)zx *V|S.zaPߒ/r~[[}n#7oPm  &@Cgɘe~0T\<=OE+C= 21:&ѣE$|׏tRrgnOQ iR(O/Íآ0+ƌ|7X?>rx9%K.rVwLZz"*Ț0$*h-}pĬ$Ok /pIK=4;NEDd$b.h0B&S , u̍o2w-nɷVKo_J'Q,G#pmC>5apڒVtsM1l,|b DT Fƍ)MvQ,!5e]Y {HT M=Lq U~0bo&x괱)~ceeL $%ڴC$o[^FoV~-MdK>!iVB;9wu#fGmm%D4f:@98^f+2"Ǐ))X&PC]%|Ppw7$b 8 E( gMvHiwL9K#q`\%-P?06k|G+xZDRo@çF?`RXh /jq3 ^2Z%%Ĭ-򶓥^>wDC BJ8F9;X-=~_~_Z91#wO/zFZgnL=VY\vݓNK/Xڪ{/gJ%t?dg:iAo?Y_aLP4O~Mn򭗘t`|?yjzlzmOݐ/(XLwu].OluXں{㖢 ܼ;s+ 랐voz7QcGܕ~cI=WoQgGWڪk,M|,kNooZ4뉋ӽ_Zѻ̜a#NI25*p]!Pw1Ƽ\޿^4 ^Ź}jGtMێ531At]UNXwհ)i=G˒k&=୹ޛ`V@V98MZH+<?m!;ͳE{:M'5x\v̳ 2U`!FPAQHG^ W<%%`:QYsu5:Oi/Vj(SkAԈ랸$~P!p%KNĵ{23CJx,&#jKN#i)__ϛ}eb>h˯3)gm؟兣5c!Gίpi#|˿/`Pڣ6ahی9ou -_F[{:.~B v H.>/pkO.B E'IxQ9Q>m!]Ci_ rϻ4≋PPehNӦ,{(w+k $iMeۧp gltoz\P]cv:CM|͒P)=Oɻ%=YzOҥ?ct<ȉd/q! Ԑb$΢0%Ǹ$RSaa&Ť&UTx1WcpM(`H `z8iHj\dO3 />_^3f(#ֵ=3p7/6]=GDSYoJ";k]Y3Q/Motp݂Ǥzg2fN>.MZIC_ _DJ}.nwjz؃0yߟ}gD7gtk5f#1m)w<i`!!+$Ϗ]y@䢾Hg\f%Q =FƑ9-%y ;CG`G/8򞴮em.ìece`N.7=r~zd\P^5G+)iQ̿&xl<:5W[5>oϯc7VJ|,R> 7=8@ty:;)+&og?19}˿/۞ 2za]cS#Ǧfzvw??/vVfLI;+ݚIᝨ{4e.WKXQwwL :hЛ~6u)vMijw>{Vѓ\8&-`4ZEw[|"+yX̅DjVHƽB;OVaա{l_h2ld`Z!V=SX> +Aǡ#`gJ>6æ}ǿBNMћ\Ťe[}_N﾿O+F_O 7=;|DwW@EӋ'!'د' L~sPN4%•pM4sC@P ۋn8YA*v%.ʣK!9GD=x(`ܜY,?#im&zs~4ipdc;aX}nTǸts"qONW=.;a73uc]_W;=Yjw??v_>2ЉV? W*a҅ۥ{;\1۝^6U#?v7Ļ~*sߦڦ&Nw{IF(9 #MCn&IT4v8L9Gyi3%=̰G!;F#*1I)j@XѲWM%M<nHl*t:۠;D[.!L ouH:a`Kw邹3ݎC%;cJECC7ipGz޲eyivb X|4&9|i-\Xk:4Ӈ=)M:Xt7 럶;qCSͻYg+<'`o=coj~3a4}%xX#qx^S~ݜrlۿn`ׇQG#G:x_U,${Mg=o-Bt+V-W?v!k(me[.1[}P V<ιY>yv"Kl\ɟ{')™ז:ahW=kl =ν[w2;Rq$2zI>6. Ҩt&xe2*Ol5| pI[¶q@{2#rl]\ʥ<"3lGbr׏e IS&P8KX֎9sl!vX@[o%]yvNq<%JaVhG{ZX:羏Q P3w|[Gs+J_V\2_IG_]4i9ۧ?vSKǾJEwkқkK|cKW?>}Ч#]PRtBG5SO?e.cwu&Q(FdA0F!;9&iXn3SLxf(GfЂ'g+qg V ik|5`t)V<(0=Ja?3M=oo{K yOM%%: eɯ*Hy쏐K@[*p z VcO=op >c}r"(օ~ȹӾa@/k_|LM%`pc  G^]?tDǟy6?sUm :SO&mәpO!.Dz]qUY2Pڿ_}(.t!3 /ZGmJo~|.w ` foq`'[|Ǐ&hf"@lEeQS -6b삑nݦ!3c#,6:20ూGǟ` Dʇp&ę*# E0Ђdlqw%VFJTO< 캸|Gj16#Pʲ-JOxu\D]F"Ps5Gr{ bX. 0b#^Om>Y1 Z !U(W1&xhmoslk/+yցop\+x@b}!n}xzvwGCsFC8\8|p_\#4EC{1V ~؊߆gxҾy<9ۊLw#H/t0Aw~P.r~M[_ca1O;^_/ }P( ^(s`2|3?OOZ,fIOŀJ\8 ؋`>4#,w*L}Dž~>62 @˃\q8#e-DKF v+x+N]:{6P=x$d*BDB _RJ߸lǤ{÷Nu#>Kg( Lo~v[VG!=>gHx&xYݡ9C}>{V8O[^% w 4 O7$b@/Hz:uN?f?+ǰ x;.ׂyɒz/pٱ ,jLqXTYZbNC80vkyl%RpGT@\]QI7V$*G墡 ~bӘjZzN*o88떯 (p ^_  }=Kw#j\cЛ.U /ݣCgT6 y =}moD]9X5J0?‡D+x(iÚt&7Nx=kV _ɒۢcA;&| &Y1ݫ$ie>Žy^g B>u9M;^&%!';1"]s5;Xn?2х;0Y\d=xrrT2ot茧a: υ y5$E>T X "# }wɃc;>aoDFі:ɶuɷW?t nI\38fS6Li.}.4Sv&N*f #oF,/Ey_P_R9~l{oqnl7nw|(ghHljuFtJqZmNA;SVb@!S3}+!CbZc6 < Gx@hT+ߓ&x6P-[|دIEpA`g\d|qA8g?e';ނ]-nlӟ!ߥ=霠C!xH;Eܢf`?sv\ GS7"+P,u@(/4+(ȆpƄ! yg"P~E&]'dBHbc?@K>:EOZdO YaE 1^?&#=03Mx 0 uȉ[wR!gDG2>VtofNcAxԗ%ă1%[[mK ƍSrGX+4#F2A(C qR]7Lb j=i9HyDhl靎:VȄf*Z';&'xۡ[?'9KxL Q]4"SǼ@:, ^ bxW/h Ϗ ?kv۱uzò<Ôqgyw1QFi~+[R ]%x2-#(‰2bAj@v2-k(:&xԒ5E5& ddg[@IDAT<\"oN=xJR?nr[Ix_Jۏҿ|:VAC|EdnF_gXt`E!O|ln']~V48Jn;v@Rpƽg~E@pG=)٫%n87 ˸@&r%++PyI5pF$aSp!P[1Dp<鍼 6)RQ4+K%o#ˋۢ-E 0o'{t;i|B|i|m5(aJ=r3q\6_3uPS-v~8i75? cxv'ߙ8E?@C/=xK{hyూGY.8=rA`/ 4 }'.5HT>)!>zhbN˷T,yWQOwDxA&jI(BKtsdp=o>F `lF'ecE84de?r&| 0 句Nd#&ӏc`n-?s/q"oמ0^/ل_6*~/2l"і/ JI[|g&X gꝧYLb;uN[519FBxOzAx6%|Ǘټ sӥU?kEh_\*KG魻2/'԰?.X߂U|߁rc5D@=x=b08S'a8SuyE{{?Uaāp_!qC-OV#EC 'aлB[ !A,膾\AHkU] eN"f|`G$?DZU ۴-yg8dƌ)Ƽ.e1̵j%26UqABE?S+PkFwZq_>ᏃOO˒Hj)1' l9ߞg'oiב{[uAb|L.9]+w5_/_<]sH7o0S~T\g8TD7b^t#l8@ܲCg`dB2f*^-Jq[p*t{ۙxRcWf QYAo''toZٲ.rѺ8s@YX V. dlU l 1 j(7*ɿۢSxo/)&Q!'xI]>}>/'dMBaiIRQ<^7^y3ͣ܏=<?lE HQ-3VV7lm:?ENiKC>'k@pB/ N&u;^}jv3*^DB>o?L}[޿bS:E߹o~'`>ߏ@Yɯ[T^"Ŝ_ Q4&YB*&R]yQcoH7t Y{,<@ڢ'aQp aI /ȫZXy1/$ MJ|x!I F.,v?Л~CbxtrS|:Q<j.6W*s#x 5ݽC|~ ^ nw9?zl{m颇ǟ]ϥZMz6 O3]?7V^sQt’K8O=3fJ`/y޻sBp$ݭ*S᧾E(P-{<['ߎw^4Ѹo2EBv=c$$Mk|9J׿&l\W}1/.w \pu<'Px zDi f<|aMp)-qmOA)Hb;3S ~.Wr!hzLnFZpA?m:|kM;:hŐJEx͹%asfHzm(n֤S"+&`:ć[!  Opð,?d#k ,: Cwμ Ûlo/DCc=nY#pb#^k%:օe9"CV)Y <.P|Gg!9up: \fvl\`Ҥ2w>ݸwbcX oLñaNb m>GvTysYeE|tiS!-1L>N Q[c YŅa2E nN& X^V " ;f7\s|&vM4J`Ӻ߃,kv95W>%>IAt"Jk ]ܩ < /Q?Îcm=zGُSHG{ʧi^z 3ڋn(J|:eG=.!_xL?}qXv*vL~fSg19hى8ݯ%?'j#fԁ=`9xs_E #M1MU$cjX͂X>; 诿(C6&74BN^@^WMR|ه+miݯ0öm|Yņ:"=Վ"D'w3D̫?^HWĒmpqV_7d_l"{{z⥋E rX(lMP}M]cfiyx7'~ )( Ncڭ!_aehO xH5~?}O\B;W}f.z=% s|Ή@~.)\((vzA>򢮦9-A2wԞ}3Y~[VgY*ao~zg=ӡ8rܿG.%+ G~N~vl'@;"L&}?V+E,+Ƃ5]m`~%e4$&'@ G]y$X2M$DQ"=|0e9yag2,ly󮄨\>QU8HpW8EOuh鲘7bEzʿ' 8խ3M˱k&aGӤK |0 9$n)zR3Ыd/}΋bgA<^\Ɲ [oJ~^x|^`zūp_n'=GW%ρWG]|a<Ǡ6w cm{ :KvT 3ER;EH|3 H(wF C=FNߍ7߱Q+&U:%>;]X<3&ti@~A4=ק5,hܙ{ [nVI`m ~>@Ms}q#^mK_ʯy'=V> /r_]=,g 񕫔!t(FB;fIբ8h! !Uf͡0KV9E,y)kh{p &x}N$v/tಟ`WІc1E _#"tR뱣3_x 5V1m56VPA+x |_|WbJ[D1ǠyVG H+i.poQ{wDZk='಻ߔϋi(~ŷnXi.| Aj04{({!I}jHxoAgڮ=xfkrqA@KQﲫ1ޡ+EH * e`WΎ{zW@ƈ-mpaGi@BѺ,0~1vXP/!J30zLePr;*NBiaoo<||ᗹ|N/?6-t>^g=0Ei|sn|빋4:@ʐ6EP- q1/:uɐdc?gsbee#/90DhzOVl<\3< T`e+)b Oя~uR+v|u1>–̒ $Pip4ʑcÑ?-z3f^"7_|%Hs܎jFlljM b vpsY+nۯ́ގ[oXo&1܋{oGtvG1.&K'{ߧ{ށb>|wԑb<Q|aS)ztM ,-/9TEholmK>*7"YpOzK\3Z߼~Mٗm dւ嬚@$9 :nZ}ga#-a%AH|B,d#Mfh-+:ٱ*\ǎ $_s"E>kt@밟( !'||+X ,_߃ꦬvB#s2vF}1&h8u6^1ʑࢲgIVjlN6MWZL"XW~Q' BJɷ+-=nZ0F&_,6-@jÐ?tze. 1䣌#V ,JJW! L֨mƩyFcrcbnz ǥ4M3҂LBfP$H4VvvXmu;8rxwj`!iaWղY7dg!D$>pT$ a +(C+@-zaRB1╳,\rcJZ"o`Av6К2m_Z&ܢM7#6oN[[l:autx*Ep87rb?h|(cdBje.b!$N$-j [cG|OS11%igڡJ@9Ve12]C>γ ҆/RX 8;nDg ՎCvc`*_d'QēKQU"#5~WmY#5:^U!´eMa@fx58h&XFJw:?CnCWCTܨV W(M^ ouV|K _[íQrӘ |֗dXwP)/% &'x OW[!Eb CGjY`BI$rS?pM_a +f٩Q2q۳<|< 38P~T@b̓ui!HA涿%9p_u51R9Aķ<)Wmo*hmo5' I bh3RfA|ljf@rcR*_Fiby  }sw̓"D9O(F'w█@Ys7-AOٗ.TI@_ĪN"MPOOGқ)hB= _=0@sJd'Cx}MwpGioZ}\/Mm5)k?8~W gī([@Jm*Tm|W"Kr`&xI?E|jy dcWyWY4m%ӊ>t7V7+uu;H73X&p99anAS5qK>>9VS}6SѹJXC:PZOe9 BP.PdX\ }>oO5 _e!肩rfÇFnn=*bi E($p,κeD À8 ^49q! onJ߃'UU+y.i=/X* t'F=eUْ? #ڟ3 lo_U`?lP}Ǫn T1~ċ&c|j>v]oE*^UNBi-qDa!WbN%~P[ur\{2jئ E4Qpإ Ҙ1EopH7#vONi`l?g!yay3x#OvTX1Dpx3o`ᴌ$?C%Cyݲqʗݛ_L-O'֤/W5J u%EN#UoZ"㴷 ¶tb`i4Sld` e Yתm)gެ ` ٺhD6+Xq:,4i39 @UO^e[\V m򑯵?k2!hEQ+WU_)˺x"+&}Z-#A/g41bdZliZxI?Evi;KgC! 54u\QӦ,I9#t%~! _w#?OQ#~gZ$&_v'# |:S%.t199BK(߂;oxsGz&PfeJ!VSQ`5;=9 u>6 {\R$CR7&-抖1igrRN4egQƤǪ24"J KJ8׃M<>\[Z$ آf6{,莯Nfx%DXBa]ڟ^go1Kn(% ^H3yߒjqŠf-IGQHk~"E ՜J剋ԃd~૰ N%ҥɓ&miyXҏ1O'Γz +V,O>Lm?  *Ϧ׈Ю,9ɖ?.|~߃359?a7٢r,?+?0۩>W5pP௝-WPQq"9h9Q| eG`y.8)97-hx݉)a>Gi+H b^i[C&d4qe}0=xQ#[ViC0x ɘzϓS&OI_[CБG.Yj0P]]i뭷ͽ'aOFnx: b'x9(Pi&D?Xw(w=yԉi*llBf ̺ |x`3elQx8.Ċ9H5>/).WW XLEd=j>_4KFe.A`wk!-Y6n.;Oi+(}^1efuQ- _ps3"6 xrq `ofT"E?[`}]J $!8Glݕ~A~[: [T"c}tS4.l܃ڢG0l*MmK~xoBWHQ?H+ Sm{·c(Ҋ)|90rj+x܃HnW-Dc,Q L4 [ Eu(oMkƌWn|M )t[zSb$M+7͞-x}Ƀi;TC mk?{$M,v,۪YTbT>l8<ث_sL® 6'*Bwc͛@QA +`Go~ 0 ٍήgi ʧ!N3-MZؚvn9,VMNcD)M%llp_ ,7m3dd7fJ4qBCaG._RCpLrZNBpBC]IЊȟ0+xr&a+4" ž$ _|ȿM[o8*?g6p @aВ7n7"=V8S_ģH STѸb7VU+k><'P^WWJ!yr,:u؃mf'&Cמ62 g}@W(N P8|b F?كoEP)>\wA6}VYzoT7,X‚nIL$ƖĘbRLSFM, cA"қsΜs ,o坙3;wʝK:TNǶbQtqg |Am֧ Q?L#0mI@7 @~ |]@P9.qx2k:'KUS^;a ׯ=b)w_%8uP{h#0}E!W-W_}E[SVZ4׼/@ i٪%Kf95_Lōbrȉ'k%7 @Os# 2^/Wz-QՋ=PVCנ"m#ViM5d=-i#`Ǝ2f=o kr1BR ?Q4'dEk^Ec uR:C &@$0 +^|V55?^%h _7PptVz_߬;>4B@ֺE|.6 ň(]M6q,]6o#&?FRF5+區Yh*7rAL'bޯ#Mh+-[fgWg|(,@B@Y90zEKbh yׇO"I9'3a'0/us=!yk۶{&|t2I>c.?}(أ f6:zW5$#'p q4ރ! !fĴX(JˈZ[؈ eGD(L| 99= ~hl]ujdb̄W;OȨ0MaFcQިcU.zUSRYʳC磃W|Q~TzP6W^7ydYʵk7>V*pZ:B"qe[ba}i#8[rdN1@ /^bb q; T;=x=i:o+l_4~r? ^=XݖjEM[7e.?5,p_&osJfR!4ϓ|`񕑍@քKO.>D!|ߓww|scl-Yo7eO$U?9DO7EFƇ Z8%eVY~4Y}=6'Mvj$q`9E0Ͱn=wތ'JS>q. M/|T _Yqsc\ /P7l7d 42gI\;hK"Zb;yCI H7:OqS܍)_~)⳵L-Or <=~v q ^s'KYZ^WQ\6ԃ_H>l<>ǏlJ|L|FJ^N-^"W!DL6/Fҡ :J.\/\MŠz[5u3jBl+qAJ+"+%G(0Ykedl"QƘ\-eVb09;h+q03CRe0x2+WPBSnv#* (|ޖ7qí[c']_Ŏ2؟Gǂ_<3pHTsÏ9#o59yGBj7/w; \\}!^ք2=0q1Mx MH8_iq]`RjNa)$G(pO?LͽO)d#2W_PL9Wc)qdv +$}RA>Ϧgvő;L2F)WfRT صD DsxG \1*ov۰٤mDZA)\Ylikhy*k]}S!_wVoqh[vJ60i#Yr%^VQR%4$]]n 2Z& Y 8$}CgB~mEi֭J3,NsRMb 8]ux냸,SA[S1Ę t=#wAMʔKdG4gO|P&&K>?1۪uXUm͝r7SѺ 7q x>0Hk53x\A~/ GqB[)^_' rf ~7H]>irv]Wm [`yJ9D2eH+xȘEYW8"s8Ob ~W׋ɯ+%c>p >`vu_9[x}0FHTD494ij;*G Z4>_1$&G(2*:_W΋+|ۄ uT_FTϦ73o#HFIfH4\tDI:Cc$%IB4MźKZAw e3:3/}2h#I;maka6je3߹j\D{L#K%G]qёe# pPj:&G"Ijr˷e:feœ씜|aesMwo.x\t<|f\B-d"_ȻfXq=dh9LT!دO=3WW>gVZhVT٫pםzLѻD3BZuirGsp禁"i灼hf@IDATḱ5}xH:c\YNkP;vo+.O/oZJ oxރg'Gպ\D'a&D(k<36E(NӵFG{dždAx):;t=镡xg>G *,7&V_59{y Ph jnm45/DZ5H˸gjokP#Mm~ ^BFUA>»^o| }T ռ]Ft G_;iqLwyA{Pp0ICFς5+|1X73fY}MN O>bwK䳃wJ9q UMgM2P&Qe8.?Jĉ^ .}TH='1EY>{v"̙'bQBmGQ~;H$يey]X餢5#gY>C~e ΙY A Om,zbFU  8+a^"!C YiL.1i"\2)^W 'B)epE/e,iv$hj3t ^hbh.e[(#%GZc?c9gV!mBbXmP ЌLqMgT !mhRzlR&,yM^]SfS3[/|?Ec} Bj|1 1ٹSKOL^v9:}eʄ[oUwlQ7DڢS]/@jͿS?iC~Ɋ=Ø0kEdh~|3 =Ё%jVW~Q4ם-7@ȏy+XV70d7i3=7PX"K+ew7D&݈dUWLz췟2o+H~3쪫;ci7b ";(hW_:S,B>J E"?ZΊ<<7h (fO훴 }Q4Dq"w̾Wnс:$=R(r2]b~MNi>CJ=`ߦKgqPmӞzF-Lt ݧ!]$oK_Px#oѮt}7eKK?ǰ`)*W6!߰HW}v>}BLjH{A>*OUlWr2a78Tָcw~:uUIInxYuJbs~=*܈3<5@ʀtŏ84=;ﴯ}krz9(є'#˟ǨZW%WN#HUt]ǎ?M&.]<{w9{{yd.:3"NV 'GXfC\A[Ͽ&L3J)}(s[~0^~ƍeWm?RWQNJ[~o1hc{j 9b+u:Dx<1hBv:HntNIἘ-[2ۯOX{Mr9w쿓uuY-1zT6}g`M M|@oO ~xm%A1v]Za)<7Xz_ )Vz'ibp¹UZo]Y NٻkE3t@6,z3tC;T#FZ3#Yn_O>ֱL]^F>%#Z7;g.ޘ]NN0JL߼wOxsY>nm j'~(?^+&*ي_c 4&p]>Q]~!}8Q=sTW扈ڟɀ!^&MZLYBm|U2ruǡݫrt#sUS䒝.D )^-(5f/)}.zUHy՘D~.< *ȉe7~ql8TƒaAYCAkr7bMdk(~zmO9YM|ḺP.]>'NYs<ȑW4 bEǏ>&eN7a)q\^@5Vgg<ס>+&Maސ9E?H7U͝/: #̑1zO|QO~{S9l ӾW=x{KY)cMC;"LD ъ-?ү2y7&Sqf%im(twPB= .V*noH}TF#kk^2#gb ~ɏQ"<ĒE =2R*^0 2}meҢMkl2n$_~#ε+@#ZW1cСVqp-1?9fˊ dsiޮ]o̕ dq{rGgGo7LZ( S5>j|gSO=>~_Xnt(vHk9{5V>o+ <zOAҮWOh믗ryI#P)O =8D x-icdG96_ڔ[~)`5k#PϢHc迪tXa!#2*.~a?֜e2Bs-)5@x\[&٘HH29}=<=aG25xu qC/Ȃ7'a]O{F~\2b{?+yͧ,$*u!UbPY;c6&`КMs=#W_4l4,|WiO?%y>yr :U#ڏ+s:z7ޔq>I{^)Kq#_<`9Ko]ws'Cn'gvyCu,)pr׾Fd>-G7燿ULخ=OvIfi|4PZw^NndҾ/ fϕ'.PP3|c8f`h Gk5 ms&*O^{k~F+w>J;YB|kf`ӌSV{ kQ('jqK!!e4 b]fH~K,Wl()B?~ DL_qqN;kˠ&.1+G NXs䶙)z}v>|^;f-٤= \گ03Fk:dpx%fmӺfUrojOx-'z:xvB@%E,sbG*eSߒ!Z[~F5 Sl#`k[##xN3=Sj~̟C1Eϑ8o`vD]N ]p!vC}#ߡk7 ou{3pף N? ~Sm'4>{w~VGC0r᠃?GG0rwC;ĻpF`y3_F t"{ob]У~ MF~s2`H ڍ B]~K\Y Xg^3,9@%͐јz'B M#|8omHuwqC,Xdׅ&k0LN &|PBbcz Lj'ϔo?W|[;}?>&kWVC|E:؃K?zf ,yB5>ʦ6F!}gSAIԭ}bǦ渮 P".iCt3zd|5A~f+E_+Ȑf#Q\/ /=~yrEwì /ナ6":L\}$-J=g9'P S/c̠rWъ+HD֞0vY=^Cz8닛"L3ry zy0uD!V458LѳsәNY-T"aAjX=+;o*b+*~vcu=^c=*\9^Z;wA_foy8fꆵї]e`ӳ#<y#Y" Wӟ<I~K?+_ydPԅ߁:e}$˪dAT>8h^Vĵ{aiϭ4~3oҼ_`D!OʋDin'({⪕ f/N؋u+94&-O[x<%Ij;O>eMImi? aG/NÇzTT!۶0eAGOcYl6f`ZFH*5K/8_² _p8֡ad;sKO~Tk)0pP8}H6a>NLn;+u,np @C _PK\xR]/X?$Xu+o#Fo} NvN;-柸 Ya:xY|.m_% ffÉ؅[Xv .7o7k.MXW6O!٬ETp`kFc1ZE>b(,qK{,d`ib!VEF7 H*š.fy8Ečpc~~ =+rtj-aMw1ob]{kr;Fyݲu|v=R9`WF :/ej?Aj/jk^s[l2BrN3dHP{ZGeFQS ݔzFc_=*L;b(/i$lgc< 5FbDbF:vEd~MvI-wútT5v?+X?tuF%5vy[c1G|yfx7Ǜ'Ac$r$Fǻ7^q߻&{lz`YbHƎe nq  |9օ/wi< k]?g;Pv㭈ӈS!t.d,F8Ml1j7!_@e A<2&[)f&=.̧ec>rW{rf(ܻ4^3d|*x5nֿmI.:uH^mn/Qʊѻbt!ͨIv Tv56:7䀔#'t@2fr7tRW=40KpH)mSNñ/[%#q3pEts^ fYwL!f ףVcC:3Ml&/t@S>L x9 ' E|דo3t=^;WȨ&iҽ`4 gCOQ@2xȮ|MSޡ|:!̊\>$-OB=}xPV fQD.8b&;֥vSIʙ vss[P?#[T砯؈;hSN[f,|kFZwy; ('#rg>XwCsWw{|쭇MX4kFsM[3OǐκI4Ndb6kź~-pYqs߃~%B>|+փ bvGպQn!6Y0*"_97=CSGa4/ʫl U7C/.Ȇ680k|B(3 q=">ӑ8Y >5mWV|~6Nvpߟ19G?²NolZ\-c]3Q?Kg` 'tTY1;{HrOѧ̈́_c`3 9`͍4E1k*+:9vծd ><)vO苯4OtTU˳yT/CEٵ|~8yrڢ5<7@=ke~ YE2_͓v?^jL (䗰ڟsN?ٍ‡MgkbC̈0wji8"-R0.-:KرB}Ppkߗ9exn|gxuUk?{(?r ndw96ٽ `z 6ٽէ(AGs k3^.<_׼oJNm@g vR( q$D:^έIzZ;sAEy.c.Կ3tXW^>5guy ~"X޽YRl MQL. } z"w^g aݛJ_>w;ޑ߀)Ŀa]B" r] Ժj碷karAgKMZ8 r{ȫ ki'/c>2hoUry_없Bc ** ]/Ҭ[ rflEr4?4'Tc4W56ƫq/VL&ۭϐG(oiU>!@{] 㴻A o9,ɌaBm$OKcU7p5@pT-1T&P +f`eZ~ܸؓ1Q^oݸB޿֦ 2xl׶^njyЍd5f$ݵo_ObE@)anj@Ğ̕%~$"d*2Eo$x 1(%;'OX%Kmm:c618쐎;;Cof6vU.+1^X+rNi A7vY)~`eQ,sǡYvZ#~(U$?L>ϵp.>aE*\o0"Sa4]O>Y+СU:a=22 J3S[Γ?\!mBG^Xps?Vi}S$wu],x߀O~Ǻ~%@9wOfI*U갾@Xn{9/*zd1#;XH87яˌ<.#12~]UzDXqV do>M1m GG|J銍{r9s!p]lfCI~=Vy*/`:wvr&~H"f? o-愇 (`$^}v~a4Ҡ7L7%M+D*e2+Bix(w _K~*_hn[Q8C-2{u=]iH"|M6#xLYHij&ܥ-siDrGQZ&lTXIv{Fl#zόosٱga=ٶ FGC~Nu346mϝ3?aT&ٓ~j}G|%zMMe&Xh߳x{tlkT*Dg20邏E!xªE68 Yz Gc b XMkѥIoyL@t7غtJ0aa8Wstsa(_^B]i#W.skE>o~գHWF-?L~Ifz~$`KUb,;KN=n-*ّ(Z`J-sH,)_B+kѹ#䶑_|yX{?11K'w \wL{Wn{|C7'~==mGCC STLҊmNc3k00HPep"`ĥdITW8SO#s52Sw7dFo.z֏μbW_%~M)ލoװїzꩶSm !xg!-ZGF~ccfr4ypGl.X_2Qw¤pkjpAV-was)q-;u\Q~}2m"*gmP護e;Su{`d8n~g *eXٓ (DN_krxHGժt\΃ jD3^NbdGBv>DfB6 AQ!.TZB@@S e Gm H~/u[ph~.1!m?mk|6 3؟Oq 7L cf" ٨=ر7qC񵾮|% 2O\ wjSWс2uΆipeE '}M7?CQ6Ehprfԝ-ce8䋗uYak=+k5RsI;9?&;%0Ryq?z98W៭XΒ犼䇱E⭢_lGx˚x5l& Y0E`?lY&o ޑbw /xVb;wƉz;K|qfoc,[ N 3{ 0APr}<|vTmrM`KT$,MXF5蠕x((W _7!&]}vd |)suq G)ԯL9OF9eT%Dϫv%pXΐwofY ?\[֙26:m䃾cq@ԥיHkA"ŶP,a9H;܃L˸_.bo)^Iy_x:MZ]$[峃gk ?r[qfX^-:G-OFl]4 i[VBdh>-43@+;A2^SVN`yliBl+qAJTY=0Wd_fHC"'8r=[R; fHCJHس l YP&Q7]Oa$#y]I}K>:ސ 1XW iSKx #tP,e*z*A~ڟ]Ek#>,ן;a3,+zy g_pFeuȆIu&'LG|v@ l:WE).蔞0CC!‚~NO< FH(i0Ob浌",ԗR'Qm%[ߔ#yJ_2|a[S B!x>*3A#& hbkbLΥ>ur|Bb]'T{K}Ov-$-ڶxj|A-ivNnؙg=uj+ˣNs3wA'+sW''Xz1' Z&S 8[!w59(}t},6p8u$*()ݳ'Ys?I~.@ZqmSfK,$aCh!Icj b8l؋sG|%Aƛq?z>r~푯XA ۈoOmx'y[tCJİ+Q ?v=zȢLm#?WLO>1J}\(6'߮+q8o̥iЄelK)(|w+>=+&(,[jHka*3>z|ZF|қN=U;ϯ}riwT?}qR|UQv4![_nG^=2pg!WF-;UVeŸSwwb]oc}a\ *W7K~[<(B4l;Riv3hwU"6Fxpyv( vo\8OhlLD1ʃRbYQ.cW" 4AA0;1ehARx1c~) )?O9!~oL~W?kʏ ȭ#jm?nǩ8;4;~yŗS>&5\M4ԤO}.m$5?hJR!qj x5%w26_{ѿ)~(DcL(K#1Y1<*?TCK80 8Ч{mȧ M^+F?O}[[l%}W'&Km1eɵ$F\y >;< մ7P[q))qr1x9qG~.<ްӱhA-fwkw:/ɳ?1ȇm{}3 :xoC.L=?%+ĎT߸1Jw_q40,͓G9;83kskϢwnB D&Ew91ӁNgbDxe넯*2P ~(@&qX$%x~ly![ϝڷ{mBHN+a 'WWD5r#xjn+&+l錗蝂Bo<4 1c9Sn/Io]GҪs']}}d٬_dK߶[76hc_Y6gye,t] Ԓ$vKj3{ >PZ\8j|ؒӅy-:vg5O={{3dd1>fG:[or@IDATFe]ExT تvmϽq@YxW;$mnopueޓ̀ 9%Ҩ{)۶&;w w-֩_͙70}>)ٙ|x~iI2w+^ix(W0$oIvf F<ծM6Ҩ >G~pf\cC9S7T@Np|SN]O> ߰nfY2ky&SW+_=u,1 {96剱M7GvN?MIes/keTp!AS{<<7P Y4y,|mw_^4A>1)w:D&-L?쳁VzӤo>>pd6mX-`UW`6d>e.8 2yhEu VC1Eko7W} _6o;%3EN${`uno::+ek'tB7mp $ٰūXɄ*uhs%cgCMG0"rHDu\Lkdl3*iHq/7fY%;ޭ4h+%oXϙq]KyQ_5s/Cɟl%%ZtA\{ ZG}ՇÀ7=CHk@_j`"a˥0=ʭhsx8 ̖ow>#GkwMwV͙'oCw~֦{7Ȫ' ⇆jx4+0xȚõÁx0K^QRoƠ[9eS|3I HLz!lEC.\_M\}Z`fDܧ~zsLzayODߒG)_g 6+X!M2t]5ꑫ(դuDLeWmگwݖ#=[&"{P;rCwbJ۶&!?P,~.7q ]% 9^F&UeM[ e-$j7.V -rM>+B\'DMHAI212գ}PujZwpժUϯG}>kswFX)pg81׃n(Sizoj8F_Yp7.Q$U /(bxdu;'W]%|ڞ5yg7+Ga d9FMv>=o6~#M0}wS2u=+ ~2WwzZ=T4kFXokm|ቓӳ*#Θ&1e;Iz|0*K > *QVȟZ܀S~_%Ca>/ca;hY=tѾO_Yw~F6M}^{- rz9dL;F[58z??{K%x[Wλk1t$g}A*_6nNSKN|#k,9-#02qOLG^?bV)P< 7@YXyS|{j?èӞzF! 3eNy#:UHd%'AH Yob݅) 9O}׿ >K-&e7 \xn:֢2VN({-pO:i#~ B΋ #5<)~]Q`'nqʯO5mya8K0x= q2?(D&MyTҸJ΀<elJӵȏ,#'J1uu8Mv,0EO:c#\+>N֢c|¯GzuN.Ũѯa geQ:8Io`mw̷ѩ#4(DtSos3kQ^k52UtOO_C^uPtϘ&/\aY;@zStJjp/%Ӟ) Xw}SM(3]nAXt:~ ?K |Ho09 _B<|̔,S6M;.' {yޖ'B}&OdhG^¦a >ULC1ÅXWnU79g3ކxM3_5,w4\t1|x30Q;x%П-F)j YjNSxŪFxwT- ]9am,CvDVeoBoe\v)zUGKPkL⚬2%AQXD#{>`ʗkdkبH}W zE~uk+='m٢;w~dScZ}ٞ {0,|}MN+Ш3- ?tYM\UjPo>_P@9#V9xyo|ݴ% 8-?ߕλ"yT?Б\w:j8FEְquAvHߧ8IѤU E~Lfǔ+X=k.Ts1}nSYv{v`볟ŨJr ǴΘxR^կ,r^/TeHʎCͦ Cqԫh#qN=wС}'a8^sɷLf MwFӮ{) 1#q 6L+ CFaISsٔ iaNȾ,&ǰC<>E12o o Ġ@2W@wR|pteҺKg}g;5};a+z;7 Zy݌9КO_+Qs`Ҡ֙ec\-u8B !fV,4+tS( $J4kW,6'@:3bV;&G|Z > 3X\.CWˍOhʏ\Is/MgE˥3l㌸x+c- a˜Mvc7?t˿Y~yw$;hIf3];-Ls9]Yـ*yS2ztp֡O lLAy0=D:,iQ{򗕷Puc>g+))o@(Y+oo*YtMv7ۏ?(M.;Nu]L~{9{/ \rX~tɠcYx>@q #Ws MmΎҰO@5d 7tִZ#x}-,x=[3 a#e3isNʞd^`hS<\k?ofRQe{=C\3z ~!@xO$z,JYDv4&IvEOq~]NU07:7#?ehKS9'vS ΰ3p\ܕ6he-cus!E>%_WV~P+ĭޛ]76]RAMa70S&@.Lz6 *WMLyGL0xqw``Q~˲(kAO595x_[Q}zyMVo_dոYS=GOwq?^ˍ&o7zvVР{|G&>;wL7w1YȪJ vd7`f?;sYW{`31lhKzʙ1Fǿ gct~oFS#Ne-x?o)V~;OB~N/Y2 ܼ9gco]w7a}H39< Wn4](vz%YJ%O&;!5_H~vi'j%.&;'boOyuMsbbӤEUl*_+i7^Yf{KU- :x u>q\@<0ҔM}[CċY$d#=G #`kuz˟7׾>;qMק Uz_N9IZC_Z>y'`6m.z+=q9jB?{WQ?%$B T@+(*{ =PPZ& !Cz iF;sνې$393N g_fQ\,O/G¢.8_&\s-e)ѦH^&ߔw.G^&q寋ƶB4p :G7Z'?=0m+%:k*Ոiph6;u vH;."';<*psЍ8bGߴn] [#T1C#Q8XKh p O~2lC}Ot)gnA[=<6i%ͤ;B4;"}{ؙy'~OV%=cɏ 1gO^g&s0Yt^䘇nG3d2-,ESK'.NC<"bw&\,.} yql߰=Ӛj@VD~#ɑoS ?˩UP0܊_U|HϴrVQ'{IȹGT{|a,Yٹ̧إ,L#^x"C㦇l˭|;dM߽hWQڛn/X<;v1=/T#ɮgU]gaⱓ;wr?raamxcsDiL.¨vĿb]lb"FrkXwNS>xゥ@ m|Խ”ѣmzcr\q=w:ڹsߣ9Wi.v;D=,c0vuvXwq0d&)w!?1Qɡ> 4uvֿ 1cvxϮhP5c<ژeEl2PksOYYc eےK~Fa QGp"[[FA6E)z nIdi^W!ЭkGǔ΅/↬.ehvFx%9EV(vs]E<&Q26}`c zLMoZߐ Nuq~y~֮c{lF4cjd5 Y`Q̛>#}Muz׶.Y_Uw{v~睧zx yJ[y:v ]u9Ý~ {^YXqVNaFN_yH.?v8aj-7O̦##?z:a~cض#/MM7.D>,Р(SDQc3xLяc2z1lHGS2NMh:c3&OArn۬&]ԟҘ2Dv5$OGҥbb:$.]~_U ?& P Tr(x/t.z( y "ҕ<"|JhUZ NDR?绐V5xZc6*JCXUJi qk)zćb.HΎ0he0(J_説By8V NY㟦SmButg"\^VF`UWZ>Jl{" Op#/ǚoB)\e]ۖ#•ȌJ[ cEc >)zJ'pMjkٟgy\w\Ƣ'tK,B'0J!nw6~Dȟy̧kF=LO抍-㴺5(KSYU(k}\,_ } 8#Cpm}Wq>a~8ssf:!2#Q,w3sI ih8k >u&-$_=υr%5g/+sZNWU ůۨ"5k5_lcA˘q!Ik@*G_r*وGVfzjdJr/=[\ڂp̭^vYgLo`'\u}7܉x`E>F䖡Ͼ\&i=񀮭d8jL=䳳uE{-CO}<~\n2¶%it%'&y}7YS^ {_kuq~oݧ9"_~Wz|zE6?F0<'0tb}aALYu6@@(YQ)6D&$ ##Mz*R0 HDNnKr?y@$әi%_>Ex|fUꐿWN]73p?kFKa$DZs_%gܒGqpQj}p12@?$]+[$>߻_f'msъw+k_rZ\E|;,d%O&ZR։HX)ބu;ڟrKF߉:\=\O3brM7ˈӮ|W} # E)SGP!1w?BFyRՉj= ƿD2PڂxU-} rH􆕢 Љ@̓K|]tò>6C;ρ"YLR~ \%$RmZEjmJz)#ma:59~l |qэըՂ1GR4F&_^&^Y&2P"R1uFPpd H0=L@r4_su+7ٹcr<D&8;T5 @`>rYB!ooaKN֔!O֐.oIWvN=XܨnR*~-#ÑT3W+ 3B2ie6۟_kߵHg^_zp>_{]a KX磧WK|ñiz68 4A- 0i)6.FNy"b& X$kNy$"%NȕxU#~$_ՃzܣQr}XYKWzeq j"%*!^E$&+(̸;($w,c(&f}&7 [4f -Ha.vl@a 5 /{"F.Eӫ8t9K&4dx_9~ 4'8(fKyd wѻ>u0OrU],2<9B"W2u\4;'DddDsVyz|X klsmؚzjbQWM[}4YfYJDGLX1q ><ApHTu䞿J_DmΟ,~&y~8U=#kIk9"^HxjDOSJvY0RqăzdX~"Gma0تnxYbTƊVuLJ[!NFnKM/@[@bŚE[]D%z6F<9aD?[wΔ6mg}P[m/-a#-U~L{<kΧ߽-s<թ  Pv] YkKEtoqBl`L"q/fn(8D@@,WģGRV"\JMK6١<GShFk!k.O#,zEU:5pNw Ś!ɏO7VmmGk:j~*c'q3H+IR>"A;;M[H+_|tNJfI='O9 qcIr5?hӥ5xO TfS#ҤlPCDI2ꕦ)jxD.iuL\l_*]˙IoZjO?Y,tzoXK ,r8#n]z9Wӿh`TQ[ՏYE+?$4xiJa%(Uy1#=,]U82tʩڶiߧ_}-u[zlQ9iRo3xȧ>uG[S䙛o3?p?%TtWŰsD<,YsObwObV9bG C% {<&|>>2J`< y']T 'xȑs 3ɾ'V@]tVFUOzwC>e=.rYt^Hyў^us FS:`1aܰ+$xIsp.+ K~NtQ39n{|}m #>i[#Uh"98ȽBdh¤98y4Ł]COv"\0t)mjA.gxnSK/Ȧ ҂忢g|ک G0'BΝ7<}YK)0p}W@'J &s R&_ ȓ<2a-jO˾X|ng.5~7Ri1{cƎAu\(沜Ws)4:Rxh5_Mvr%OHx\r+ Mv䱼Do9O%߃W5$}9EO.c_q~$WdK #1K#Jql?/, e1EB? e׿lw>IJa¡W^qυ7&LDg=oPCzؐSy(z3M knB|fGd$Ӻ<y9D <=!^}F$]O&3`1Dƌlĭ)? zse返$^ %9M'J7te쿢!Fi[R~1EYݲc>:%ٔh#! )乵F$ZZJ~{LQ_X#6u~ k{^7 ӟ.Cal\ fnsfa믇O6w w)0=fNRuYWX#H~qicXm†7U~&b#m ƃ>=9?wT^RČJ,go.}Շ K.Ze~h>;0^{_Sov9y26Zwݰ;2,Hx/&|W7اV[  MΝQi瞓dЩ[0 ջwda}wJű^<l[<Ou&;h6e8Ə-|lɗļmP(Y[[녷_|ڨ?vG}!ʷ[:n0$ZפG}XѬ# sZ-#<%6GQf-bq TB8) G -nC`,luGvjޕ]1ﲇE7H22U$L]UiB--36-s]Eic)zאt jq*=1Xǚt1fJΧJ@3y,[ 0cj:9cai ?C*>,5{xR0q~0~zl]62V yqS/|s{?9i𥗉_2ua6NYz K(B/)!9?sQ2mPdd6ױuGD~Jg׀Ցh|䣓弆{v?Dw=OowCۋXg6$X (_6;uRͮWX?.#nU NH3nBUOb0Z!oR]r&> )`#[LUK"xt9~kp kߚSCt.eOSudU'w]t;wQųE3:%vH59{2LS@A˹r60r/j.xBN8#){XFYkv]UL]M`.}a]w y'tCcmFzgn=~Z&uWxwhbԺ6o%C_<) :44.Y& Vؼtۿqvx嗕f`  21tLNߝ82տFll]{?:KadcM7Cg.F]5cha]l@a2F"yD`;'dHnpMz.ui80Z]:c$6/8n#c{Iowa{zfB>xXhFx#ɻpTk7uhcGZw/=󷿅p#뿴fL |̊i?x)[cW|fY'cf[u ;KxwЩ{w#a):O3?>a'#+uai>_<:Pb\zZ _OȐgeY >&y'm;;4KQdtfXơAd$58r7e?@{,_OaS:( +dhy%ܨk#!K4?[wv@MfB6ѣ}$:8^{4cǍRlæ-Nѻ[ )*u䙿J~KwE՟4"?۶w1tMu̙XNce͸f-,?ˬ T0g&7LEX؋3&*"'Ÿgю:mSՓF}:;ug 8ʃCưיgšv\l`6}03OÓc1r٥zǎ ][ٟxSxŻ]N`tVC>v>5_ ms Fc5 $n)}=W w̖')w_nڊ1==Sw4~%pt;8ϝXX9:<^'uP.am,4P_^Cq q |.8U;ayg~ύ 3c/XzQ C}5~fـׇ?@. Mb=0x|ͮL{ٰG?H xdTf gPbcvepQUp} ~sE}J@aw&PY<£J.Ԍ~nSΣ ZPﰨKDU(ʛ쒾&sv䓅hEw'Ο,\߱Cе+3D >V=nq 33ǔdRҔ57oI)Gd#/!=r|Kƻ"UqѸFf8}92?P/O F H {bZr{?v9昰ň¿ :4׏g`=yE;}sa |.DRd/mC.kbD*u%8J Ʌ8jEqY[]05?H&l|/c=țkt`oÞgۿqGwh` ЈCG> ~#q0}@C`,\|IK'Q\>h } v˒} &Ge*0F姼UyDTi!_}ɮy~38{c koh6HPL[s/Eӌ^M7/a nNs/Ew餗3MkmuxQo$'6#xN73Risj|Iib ^Š?eo+FgaãWo{Ccz{0k,1pā95M aT\z}S!|_bx=-;1rx0a7̽bB.à"xڔa=w/FF,`j?$<×ne2i {:a(r6oA?_6<wQXjy¿~;c˅#]N8Ζ0 }.˗Cn*PV1@IDATlG0s(l|U_&2/#y+ﰢ #]; 30£!¦{~4pKLTZrT=?jX J!{ 7>>ZD͵mDsa16lm*6ME6a=uÍ&6sm,S\pϏ^Y`4,\}?|9֑36|;kV:#/OJۀG}2P)!{a/ā2pc ^7a#Xv9L`__B{P_qE|Q.i k.CI|KYƙd7#x ߞC4KA%3fu4(Tfبc&^7DVyco,|-(>64!L5`拶c0CD_BHX|rX\)]#2pʊЧ+G<|ڕ=Lp]|'eW@ɑ. ,U!궿d'eo٣G/;9$)fYƲ)i@86Fs %ZCk(e$A,|dDT֑(bXn+V 9Ga*nqƏ䧆 7? _ljt†%N*<X334ZOu2lC:lzbG"i')_\M_Fðk:bpLD ;oSEL[gð 929qFǡ:]g(2=z8ӟH,xn{WO2J37{,)ՇP6?EY+$"Z4i$$do]vbѴGAܳG88q3;a75l2p9X"3]A{l+7fu\eagn4gLπy>=/\D9kS3rn\$C//!nq#`؉ >`8i")RaDHĮI4u<. ]V{H?3xCWхtt9O/%F}"EZ0iB-}1~dZ_'|.8^|ǨQ0~mr]#x4)|^:S|P|MZ4%g@aǴJ4Hf' :[:&;&;wV+a#R7Nocr?6vۚb]G<}+6-^^Wp'?7܊f[S2],B=u,܋Mcr)_ƔҌ#[.-Ngsg߇W(NBoaa7~VKs^8zKجx\kW).:C !E#YmYs!_S~:;18ϝLƨLǮ6<͎a<r„?\uD|d{ik b ޜd31}Ñح_[nӔ~dEYMs%VԝODl0+rtT Nl׈F=QQ`s:8%1Z/fhT#kC~>mnTLHaXL*^mG<@&.rr?Mq8Ӈw|E3gVb:n@NG]ٸ7\^}:n5]4ph"kJv Z?h~7vaH8}9s΄L#f %ՐP_oD'?x X )lR&[W_LČqđ::7ec{8ZNSA"quS#CtXR^j*X|IS2%]ዧ)~==BTXd Ngysfk $W9p+'Q8bL-<x\rbSA;8]ٶ .tX»max+ =qʼO{Y8>q_84 ]睇jF?Nj5Gxgs=l*:?/جF~Cm#ũjaR?*:2? GF;'#^xi-sW<ϵ?r>aGtQnR('G 9Ah sz;͏O,AM_T)v MLX21EoK<6t fa1f|kwy)׿Ĕ3}̞?h86$Vk>E/<wأH }vIaqؿzs.1>'76m'㏪_>| AKY4o`>6CwDt)oɡq ]J/dɒ0mt8W1IݻwY;p]UӎGDb6YHe q'``t{'*S\$_3cmđh9#3~ZKl1 Uz|n[}z4K4'!3\u-.91LG1q\NNBt3\u}/vkcr(Uo)L='Ym/QqlG\8}Mծγq(o!4.8Hp}o~So>ƌD9@Co.3l#‡m \~? G7"g漕 `7aG ۅ2+/ tQ;Oe |u+YNafȇuBvN]vD,#Y$F_TjR#=y kԝYh&Ɏ)o|:1,8INT-J`t4DZd<X6)yaU ΋I4M唹"0YIg Ҹx4QIC-jFzjhDqZkŵ.7١十,.c4}.3Hu.5w>nz\>yqcNM<&_HU$-E $2y@\ -m ʫ硅V;qLڰ Lh|//YՄ(~VsՒW|w]ǑgN-+$Qя%Bė/d\NiޝˇodꌐR"`F\%~%x/߀3.GGHK~'%_!H )z/1Ƅ:2F9´Oz90h-d#,vh_.L)eq߀HZR>6mUs+"XZrL0I@JђKp#!\MOk86 5+VfG=1??zW*3}6/%7I>Ӟy"_Pv{|g)@ Fø%0#y;6qQaXRmXHdz_SnszQ& qE)OQEw |Q(J;qGX -qޯ\F+)zfCI1>(;lղ&)|Cpӵq&1V.՗&4IZ8_`]#*E6Nr)꿯?t~ YamDz?85}G'ZMlcۜn󤏿5Ҍѱ8\*RSceB)Q~ $!C ]Z75 CcRTXp0q)G_!i"s"H͜8A;b֖yY)Ru\ΤCjs[Kr<@:?\df?\$=)lI+"J0UxbWH<oHUGz1BG K3)QwƊb!q)1{9Ckh} p=ib riH1D tC*BL"A>K4J&Vum!$y?`C"l~TJܚJt#xIbQxՓ˪q$U~oR?˲3ʲhz[oPhSZUY-&C9qZGi(CT0gI|!.]^<+˧_eGL_Ȉ_et:|'߃w=A(E[u ք|,c![K~\{VBZVe٥JPJ*hɇVONJGyZyu?R1ƞLlMRڭOn ie}ōaG8>qH58@{0#$6Qf6?=HלHDJ +|G*H~i rY^Qr|e/]U[觚HI>0I-Zr1%X"ja&fAլ}hy/7Zu#И R(h+h;$/|F0E7idwFj1E]|zOp".}H0}}e99~ǰP#~c p-)vѳ]1S>U\|[xL,M{^K3v`Vp~)M~W#cjyǦ3m&f?䦹ŋvL[by3uՂ{@3j"74sZdKm!_a; Cy+}M.^%;2,(Rlà sgZGFcdG.B[>y1lU1VhcP#P#zVri&tipBFy ;X7\bԖuCG3a\kLKxÜAR][vJFrNTuпm^{`ӲHI\6ůrТ7c2p!BP>UQbԞ*"G! V8^6O@)^0u fՊ'MvFghi3p!qgKNxZ'xD-E9aμ6!9t.$ Ccp-Zwr& ?&; y A0q㎒ˮėаec&9 'N)ED#ui3%a"*oC{:8|:أ>WYVϬ˹~UZWv=k۾X +wf/V|*_;,_x1O#}Ud+h:_}RskcI! -)?_vfo14)EzW貙 tmu*&36.#R+/CJ(diKR3\ 'xi QX'9DT;_O#0EczAz(Ÿcyk/٭ 6I^~'JF7Y[TlyIQrl~c|ܨ׊=x֯H* jV$qi$i嗵P|pVA>?63_9Q\lke| xI| ]=@q毆۾vzF\>éx<َy_w˭,|}HP>~[- ^F|](:BL>QCã eȏ܍5#KH8~zWד*y՟'WWZחf3l%X1iwAz 9W0.>qkoԸ2.6/WɡP 'S3SNAGRO Wc 04 bA 5+d.e߈o1]UK>[^WUdA7)ߚwEW,>1(xXPk{_=+Ơ 3PSIuxHOG^'B$4_a#xx<0/t.ռ_Y}o_AxՃ)xqleEo8{O#{9rwÖXx񱁯̝ͯ1sђ9{p׼){9#Btz/yK #M0UP{=Z4lr(&$ʏ+艗#A>g1 ,5X漢psz e#ю ƘO#B`j(3a97bIy9WUEb}ahCێU?sV>w~ 6#ZtE\Lp!qӱTTZZf!^@9xtOW=x† \ U{ ޫ'coaǰ.ΝfL^;k`_{#ca&fhhan"K, ӟ}tq𻬳F]nN;a ~~<%K`#~2 <>8azM=ݷ2nXCm%Hy/#Tz PI:onH+~W^oDtEߑ#$ӟxw9ĉVaAJrDB+[ԖXxt_=~o#B}|b5"\#]3+SNJ邹;.A2gTgiHe%q}c1EX)ߡRHs8k9OtRui7Ia.6n&5!_?C~;td`~zf%S^:iGBgSX90i.?\RVO"i2*p&'۬C0MUIONW>{!FEџD;n;kgvoƝ2`:<{M]C@V(5<>"@I~xC{n 4)?d2a3^Nw_Эd72%Lz0Sr{]_ 4?1a3oU)MvK7#raYQS$OƺK.|EνP]{`Č)9%nK'CZ$lT{ÀC5ؘrYg_~?,lO*O}q`#[ԉ 7w߰'~ތX{B<:;G' K/ z@nu xx6tꉩKÛW肑|MccxcW]! o?mc7q~H <ܼk5wы~6o{ˁ;bd2; `}uhU]ʷl2ͷ`UkyP$xJ4fF WpO㍑3Z!ߨ]Z65M:F #a$o}a얂ʻO\d?r$fd5xC*p5a 4Nq~K~J62N֊ya)+FZѿMvDψ89B&ceYQ{'z⑈.iOg嵰Uלʏva/;wѻ~dZ%yO[ӫ~_tс~%s e62OE=Jo8|4 \tasa=~0UKnwCq%g@ Tcˮ?_{lȰ|pg6^0!׀C ;bdhxϛ-#mPJ0qY?atO`# G1s ' ;ct6 gL;_ ;Lk4`-׼5e]j೺Tzj^&Y r.ap~zx{4X<| ȟVcc–#K[N;5rS b?wpW3}(+Jֿ2Ì_us) 9yf|ٳ1Bb➞D8Ȁp(@%2ff4dcNYHC\SXgX t|,KE&=ˣʧ)W+*_2 hd)zi-+`Gm$d9i ymF' Rb-Az-%=gK(@nSԻ|aY/nYRmcb\sӗOT$;hP6p׹ _S܃9SRGᨣև^w_Vq vZܭnb~/3u*Ot4[ȟ7'<6ua~zl1៿ux{g|kxQ5\P^F# d5#CɎ#({k OgƘ_OS|0~{ h;N{2/ ;~ X?(:~|'?& r?Bf&?7R>/8׿z _ 5ʷ-$W[>=)ԌU@kRLv-8-n։4Ұ\yik%MD:ABf?q!J~GF&B_8wg11[Il0#wX ZH(hHN>bP`n(|Th !ͱR]BWTUO;j}G(/[C~6E5ѩ5j ~ ikF"wO΃Ue˗b:sX~$ec򇢁_#) bh@ړXiFɌKf/xxo̟> #/ cU~Է;{ׅۆgo5w'65\t `cď] 55mlBF6C0? qLŻ מ; :'3$0F}O)t_<6akտg<3^:B8& ?Y^o/6o1ow=௝v`Ŕ}=T1>fn>AkOv,8T:+XBPWl |*炼ūr.^4Z$X0Aq5񊁤<?U; 3Og-ϑ7֐OyܗAVK~c *?ѳ,V|N19kdvkaSmɜOJA˹,V)]j!ĪHU h"/Hj`=қdWxU7 zXN4e8;u ֱ_aם1m}ιsϞT=`8gǣ?Q?)h3a2ԯ.;Z]ts,yXcaC{~zG 5xxd'9G} = h/G}kcc)}*L7\Ձm MR]crq'?BQq]ˍvdHK*Oyv۶|7t͸f-,?]kBG/~ /P:RX9qD8 |m֏v֑o qj2Q߸4%N6f3;e΀aָivj píh^]X eNŧq%֘Ҭas l^Ro=dUG^{lk^{#0! :??#tnZ_&zbae,a}&][ ?>s_q\MvC*aNնa >d'|eG ?7a"Lx:a)?s :<:TfcK/ ">rvsϒI,X;f4pGhX8Leaŏx'yI♒@BstZD"< ? # 3/R?2J;V4o#M&4 |e ,oxoKXY#/?2_nK.%z3Ì /}I*x[>a^cquk)sY b*<=t.y*| 6Uy8op׊1׿A-5__?N{Y__.v;7ñ2^WJW\:v=+&،SsD#o:6B=Ux9Gp.P:ޮm ^qEm0,F c ϋ[M}+܌7~L;nWy0:a27}A06v{SR`xn9xt"zB` +76月>I'aeONODCYMܠy]/ܗ7?f s5*m\!"^f{O8O h|LhXTRt.Be(n|x!6y幘gl )#EHLy̖wo~5*ϞqC>Ox*ͰjH Y4Nq0 L`A!11ײ# 2=ٰx]kȏ |. ̍,j( Ɏώ蹙D59cZo#3H-ETcmϋ}8E= oVR~Rǹ0,tHvs4K-Y8̜4%w耆ygN7mD3\lx<{ fصymc:h kAD򗈅OE@e7FFHlaZ W]~v/\v6aK|Ѭ'x_>o=tt\oG^.X-;o~z飩z"x ?m;4f1x0uI'XL-c{߾0wFF'&_ >a X#e~vXK8;ΩOBZmɡ> |Owwx𷿱*Ryld- qӾ0ǾOՑe' w taG^>FhIOan,[[f#WxE3GP9~* ՚OD c'h[UY6y3x$::lt} 争"(F8"xrxnHKOCZ .8ҹGǮadkreiGPU_|]Uٻv9(fa=,2`쌇D !"rFH |!A!"$"B8'M_cs=U5+fwX[]u>zuTWC^OF1NmwmMv|s*\S}QoɖbuɆʼncG 4Qoj~o#^'=o$lykaP'S\E ^z˽1ܿsG~Ϯو: }{^1 p5zEܙIDATÓkCc∟ʹ9wz罔lN?0}ӂa\^ml! ťCZ?pn%[Yٯ6[0(r*"*V\kɚBm,nu=ʳY+).eIh]T̼,d@ ~D]WѺrF~/ؿ~R*|`"Wvs*kԣ/dO!BP4nz4?:e L qLoQߗ>dx{˞~[tI?ܒ96{nl+~Wڦ\mP_2}Cor,j|Gj&^b<7qaWuD`OXz6IYc8 4f`Ng23l+ppWxi\.eIxUIHluuw+<ĴGY)ݪ l3,_zr)QδC8'5,} bMA,F  y#I"Q,߃/u~wUZ C \m#3FZ8 CS 4I?=8IiًW[O7nk|U |j߳;>ݥ,G:pd9oU\GQ:$-FC9`O֕ɊLmR0L K7S_:V|DپjUD)iB?ʲtCxsLs.BKms8̌1%EBR5 qyOCLOLNs~b3I7:*5"af2#tA"2'* 0'\KAHp0E03JEEϚ(?_FF`H]oߞ-4>$pf3d?DRJݺ#xsL&}A/ yE?ī׵4OrWϕBT6D7/Z"^xː*5۷9Cޖ២aHGɿq߾d7OPg$v'|ZaByǝ(VzJjה Lt?M]$>۬@?U`4=;XA6MuYHs8-O+'홻0>pG| W^"⁢|#*]!#/@M&{ӦW;jSI)1Bwpz2ٺb -e(HdӂE2;?l@i+_L"S^EG1?TiL6.Z$ |OT,3꾡FaUQ5FX"54ma/|:S)h{6jHeQLI+DVrEПNEFK_3ÇC}^yV0 U!?+e?{p/h*8kg"+dPˀ;kSe]zSS~Pyt?|?tLq@x*ȂG&2ϿbF瑇Η>CV*6dwˍ;ȜWaa{f!K!m/Pv,4I?îGGy I௸~ &$}ߚWSqv )غ-?m/5@VL,P_ELBWyk%kf|Gtfҽb,w rLK̈́OCt#vy@d+ >cK$*'ݘo OyL5>i< 8 >1f̣8HSKC$ cH#f0oSbHA>s|M9{]MRd iZ6[ή+ep^}M^A݈3۔ϖ:+CF.Ymեf eqJS)tLX$7*SzBni҆un>?F <:4)Gsp/.T_F#S Ӗߺ=n#vkG`o:⋤iI`Oǿ0_.0z͡>80o{х $/!//x?R5pģ7I!xy&&iɊ`h9E/)k<7 0/%:`Np )YRy+-{\m}b&G0c he;t=[YPvͲerrŸ"}u)=nU7:OϮXAt( 0o0Avh T"5ڴ rlYSeǺu G0h%ٕ*bjxn^SHf0r..}?MVaD8#H~?FYll0֪ZMd^z\kѮ37ը!5۶ ȶ0=U6az9 $%q5$mJٲllY b*H'H DҀ].[.KJ6peH%p!Fr*WmNʘ.ܾM6"}Ʌ,nف.DM ~|k)+Α7J*UvΒW6-ߗ{1=#H^t4;}3:خ/,I}+y'mBibLM퍔\WnHTۺ|Pu:uB{n(Æ ^ - PJwo!:`mN157ew.vI>7Fl>.T0 L$.U@Ip> ^WuCLZ0e u{VXCsxc2GSy`Z#CU{{:|8_v/Nn*yxŸӥ2fN s6`HGwAjU.S T0Vl KܣGѲ3.ޅl0ub85 "o=D} 5Ѐ|mѰNrKd]#EqxXaO0GI/9Оz!m"X WiRsm4@?)Ǵ #Y=뭷e纵˝zٱz{F+lMsS@&=,;ҪȽ|ƧL0E Tˆ7i^^jÐ>K8~MftGT;s' 2PNSCur7u*mח;L}5ݠ$ ^n=uH&5 ˱sr5 U: BJ)8߼0*Mɴm.HQܶ-10 K 4{I`(sXP"-T/%V-⋒-mIG+l^DFe4u\ߵo[Z YROaX7.D}FכoR_8zL& n 72#{tǁR0\ݻ{LzYa޺cOIvevE{ /ի=l0܋{cGq4dia#.\8?޷?SʹET1$ xğHvt&&%_3xeayiptD8jg{(m& 0!Vt9755坎v;_Va<_'Xi ~ٳC`OJg 'թlۻWZ:T߹qen |Cg !ƒ #x]ʢ岟4 xOH#\ŒPI}=oسTnX}83/d2}/I(_>Qq:uT"<u|;X32YF6~]#S9("il%~~ghggg/Ǚ (x`6)Uk֠ju+ug 6[O~R3kA,*@ؕAEXSGőAWCD*B#^=&'* \"teݴކLh)0ϓ}6~:E7WsMBi^жC75ՃC ,Bحlr~|V3{<آC~7xh?쨓rNQPLc\xC埅Q_G;d_>*XQS@c jM@_EV_>jG"Bҍ$A)$?p8݇t\e'`70h)GG<8&G_J4ۭm5GFcVt>T__vY-y;wHJ˕IŔKtNdzGҺ]:I7 op S^UX'ǎ#kW1ӿYL]jsI!\UN~k7dw%6Q6{Kg=Αx&#bvZK{fd~/K>mb*6~Kl'0;QP\9V6|̈TiT{vyDϙ'uN"]]>K J\]c%U[iE{Ge G%vm܄Yk HjRұcB4~>C̜Iء]Na6h!f|ᘏB;yU@}/u:tb報¦gR 1,k r>Eu68&BsM񉜈S:=F!i0?Ym7mjY1Ef\7m-?W3gˤnxwאXg&RTlj||nLxwmJt]^x7;5{a<5 E d7;gOt ÞM`pR/'P:[d<~iK*;"m]Bv-ؑ#DnHS0x&kq5R!*!6y4fQKO32HtW.iyKGQBcr2't9E\_6q zaʝSg[c¦W|UN~s9zWo8Y"m PKίYAoB^oOSsUԖ6E|)zPTQѕGZvL2Yҥ3Mow5F S!V>zRyQAHɽ? SIIh؁~;^5JW_~T* 7|({?)[GtU`'_%JdȘaD#x9Kbgmn3֒`$f-2 MMw˄NT7avð.;<Y:;Y5cl _dֿ xXz ~]}½ĕq7¨gb?$q DXfF?.zŭpQ( \v Cd9F4@?vbfC϶ِp@h߾:0aO_YoFƹ~0#1%X'k_HOu߲8[`#̸cVQoi2v:ݐQ0ʧ{aj4p98q<.,C=%Olٵ=mH7/Ōѭ1S6J"Yn4mFڂfk@ 3B]Af:q&PyLdW.Jbp#/7m3z&r&o5x8_}\ڹj MqPOY6=y gaܿ͞7y|[`7n{BqV_5k{膛Vb lƪ d.oy*S0-Ga(ӲD .ycv)M?QaO;E2=5.z#7)z)-qq֙w:D.5ΡЏҕōQzI<0G|(yϱn@z( 6ٹ< Wߴ?_08b|λ1 0_iZeƸd#)Fbq}eLOǮt)&kۋ05~Y3e|#QǾ*< l|+/ga6XfVI#Փ-0d!X+atw4o v.%PecOH̹錛x[os{?2yw@ 'XG'm1XbA-c?@.F"fF{2nQjw-f'7\}X.%It8Mv1<ۛ'f${SJJGp{žڞkbݜ!oaɉVң!ϐaɅ3RL9Gckf)|9Ә-?i8x$@Ǩ5q7 g* <^Ÿ>1&gĵl1L"ZDNb ) *dw%u+ʃf=G8SJG2]]M rKpNZ֒KqaV?EOR-qLnhǓ@cx|_T#(O Y] _;dѱFNeI+OGҘb|OVG+G$<\$LZYfه 7?JXQgz+Aj'\"ѸMv\42Q'}A#r6NC`cܿRpGs"o#K/!bI|?gP?cUc)zg9B}9P}OZ}\uZ'ocjYݑU;tbctݕ[wݴ:iEn5{=*Aɽ=d ?o YB7g#wd=O2=套s|:2ݡzQ/ג<^B?(ݩX+PƧh{]٧9!o}ݨx{P~)I(EFn3a~#F᫁}x?Wn9w݉sNZZE2sGVިoG:]˯?@crg1o"7JnjTYW(#=Eu*8Ƴag=ɲG22F%t5r\ ]f3'.k\IUaeݎ5ּwяg}4#x+W$BӏO*C)z;&gZ⏯a >me:DS],WJB>PsxǢHs1=5p)IѸGr]9&$b'1zˍzw;_o*S"oыK05 әvQLwdWSqҌ))KMvɩTY>f-0,S^ëqq~~g^JMf|)z72M j9ܱ_{k/׿[u3m|gEoi`>~sixSxxv_ y}5Ȇѯ&aܢ;~v^5ދ fjC;֯R [6C0?5i࿸jPvgnX]cu? :5d#ti~'қ/[H+6hctڃvzݻ^%c!:x@E.|i  T,0qξs#=nE~+h^-ڞ/A{..ref&]W[__G .ᐮH'Ul( (Qs\lv0&T?}:OLWվ?x责C"a ,Ƈҩۙ;ޡ^zï`NƻK`v?ilGʗrSS`Y 8Uwkm8x]_ue򅔫.aT^_|tȣ;X;X?E3=8Ekz{r3.Qys#xR=< 'O)Κ/iԊlqt+8\P07FwF<0|lٽyߕz$9 u׈I`lVšWd%FMgPpWX~Nĺ0>}(A :8TJ|xm3]z>\ Bwq@u聑VnM\|سx=Nﰌ8b;9c%x%|i$JZÅ18 G9w\[7k޴ -:-`ѠN4B7Yij).R%]8g̴a.6ñƱO<韝-e3?qt5}9E)=˿h ?ϋn0H@_|>SCq[㉍8WV vo!f,ʔ"K_+N>lNELaT̓N83_5:!C.`&D7i܇PFqf ўG6;{T~䥤(8~ c#G@ :kw޷dϨa:I7J.l ts[G sWI;h1=ikEu?03wG!Qa,*`_i[O i)[vş.͸eL9T» e6ّGqYUM;feK(9ei[-yatX"FIx+"33?i#8KK Si8֨_r q_%RQ)(㔘h^;*zV. ᘖfD1= fD#{4>x[PG?+)/(7EP[QUzW<+[yU?E˫D=}ҹ1bـm  F 6okbTvR2TWr^s0F?y[/& e~On [>!;vU![Y}WW>}^p!aS &gOn@18t {[(S?J ?> H2 m< RQsqN.)#͒aI pac.*ݚm.>?J% 7pC|MOoW!G>q_[ \;s h?|  N L`_0<u`?~'c IngQ䍿 1R1aJHq3>UWTQR*HMܕ7SCyg5Oc0_?U96SвC@0X:Mvcr!cNc _ɤ0ڡ DcBD@عux03g`(NKD%y(P1fb(\8ߋY,ъ#Rqsb篞#Fb#!/#S~S?0S=I1?u/7KS~.1_ <@8h>y1N?4p[0 Soc+iy睧 gn7iMR਄io'L$YZT1PeTaf0wLP7MvXfU|A65: x4ujעݯn#PGk F ѐeϓb!8'@\$\m>>FRyL}HvOl`aƙ")?ϥ&; 1Ӻ uofXӱxJ>R KwBr Ռ+)I˚#R[y,>ƟԢĊ=4=Ɍ/j}x_r{ϗP4oʨ@P_!n>oK&rYS[Mx }gR5w*U0p&ٰ>7Rك9y6*bQ9pgd=.=42⚎mMے0,Eߜ+0J؉]=bP &aGQIPDkEM-{GQ >7%1eobJKain'bo4RHň2,cU)u·w;ZXw.QK3 @N Glh)y)Q~<;En_;],\g6* 3g0 葵`!_}' T*icſ F|<6ƅS"~oӭ+yhg0T)M!o=&*ҴB_(1pP֒pCxs?/[1c/6tʟ?4\e?c:s|5{B"-@36gQ}z hfrGQ<:R#xs<ǎ\e.;#dVqy6ݯ }8^I]x4L8BYMOW{]h7.z++9J-X QhF2?ԩS8?߫z3=Fo/TQUat}'~@vjR#jW!q8Q7tEK l`M&;'Nq ֚) ,4xp8'7~yJDs]p)a(Lp0"eRf)@ vw|VQ1F(wP=vyhi)*i=?}RV<uVJ4ˊ_IRQFZ}`?ˆ+ҸɎkOl#8YGa_xx台4\bĝMGuMѫ2t%$oeRPQa p:ƸR 6~N73RdʢV-򍞉^㚅Et#S"87zFI Ў?cϞm DU=>d;㷆OG9K_b]0K_F7`IĆƽx?n)zq]Uhl;` K%I2g&O i+_͛t%`a2&!h,6 D}.aPдR8=qcU6 UAܦ:ǘ*'[#zNXB V}Sk!2 P:A :_>ժ+2+%jٽ(VgVa*v? p%ǐZ*"ZzPqفOⴎ>8 e y$?C.';qoI"Q_,Gu8/j`)1a *zm):4=-9ĐwW:AkKT(qXO0 d%AuJ"o}B}MN k]L E2*Y8mN1x147ٱ}B?2B`)s 1&(YTUR4/X6efem՛j$TTގ+@qb)t;с9EKM~Io{#d˲__ğS>_vs5)>CGƇFi40˟ 4u0H@F@#?hSEv<?ATkᷓ_(sQ)fZ|@hcD_CVh b1#?e}l,}y]3$==4Q"#HGF%\+/Jq嶾%`M_ұ|fxKfu+2%Xx) //ۈw{7^5W{f}>UO>ٲC`N_kT4Y[w^ x-CoϒWG|pr݇>W+'MrJi ; Y7 [v\+\RIetyJ4CG! iEiXS&YP+/ #>K=Ū,/:++S }MbJ".j)W׿LmJzU@ c5&ԴD<)_X YIpfȥ%HL^r4?mV!oWLD$Sz5SɔW_r1\ |2@(߁ di^t;ƀ@ϓ"m =%m7OpG#fQGXzg70@)k o@>ժWtMBR㩙iSC5| T_Q\Z@pL5F/U`*Aw%İdgSfBL mLDDP7YGߌxI( &WNjƇwzSW$}?Y,hҟՑSc2~m76t*|m[T~_6d=]݅Ns'Zk.fD2ks f_jSGzV*}}*8ƌVӲ376Oi#y5g>S<2šM1dDG*>ʫ SM9AF`wYyIY7ߌ+H[!.SC1P4Z#lJs`Ĕ-Dn:{" [7 h-|H"QP .n@T |f2`5|SCG?BY_Nc+L"=nCK-/Hq:7j$OW@GUͷezCB tw~ C3J'\9DA7ٙSy|D?Lf (8tNi0l9"3z y)(i#ɸ3BG *# 5|Kǟp_ ^Yxˑ:WF^\Ir 0:g 7"*X-j5d9})䕲_>rb}i"qKjS>G^[UBIJ tu:E&4ƣ V!_04AkA,k0jHۏ#)ialYQ]jUeմy,#UZ6d#vY-9H6Kft3ܦ -􏼌f${=TiJ D_F پTjH\)͋ rӦ}Uټt1:k+۩d'= ^֜JZVS^͌kOp( 2_"/Y&R۰=4 {&OJˆlsS7۷3\Sr5kH ]lH: 8:BӢΘ`QqYxi֑^Ѿ7yeܐaT%6krFmլ(aEIdQt T}]R3Z{Ηv4? Pxc FOO֍ 1kM. En+i!TCpy9>Go$=߭8 ^< =1#l7u&d:`W`bȑzJRtuLtܬdl 9Ǐ*H+Մx"xB4EO7>\߱d~'4t+6u͏eLi 1*U.̐~!t %qwnh(C_ FG7@^4u7k?\iMgwԳ%_3s=G}q\WQ}{F }x^k=OQ`Sɮ͛qz ɊH/oMK=(0u^edU?K&%v0+e( ꟅesuXg]NvOud))Fjq^:o 2qikުvRudg>tBvt ]SNN?V|׆0Spn֢TlPg XmN(<GYiMvd7fJMkOyaԂ{)]3 ,LzY?,S_W _`hy,0a: 8=+%gokVT%1yG“9VNmҹ'«C3ϿFb 3щt};P(<@TGp~1>M[GȿUn)O8_:MOig岘ߢSU庆e%Oh]+<9 Ҩ\W"NQbم b$ 6/kesD\WS0jk.;*ި\[v>2չzEa`hl-1 G3 t6a#ىqcC\DΜ)ku:#8]o*c䣧Ly:(gϖQ:Dt|"1ɒr a!GaD?TnPuro3ד䤟pbM7{;xJNO klݱtfխ\JYQҲh2i27v/²n:jנ'C xnUM~eީMdJƛ  8K,;NӜா̘(:qsgɧ2IݘNYd})v^"MQ?\Km?p#]6ߠW"ӻC׿>|ꋅ5ΦG5 q}"⟶ ]srCa(p4Zcj N&; qcYWB>▄g#x&ﭛtr0 HRܣ;c՛%n9a\_9žS3OΩ+ȿzr,#yF+e7ڿNEt ʉ$4aܽTƾ;笑TuX"f w6hy^L_sY┋47J`N|Ktβ)zK*oSi$Qs:fMvp:a0b-4ٿ&1)QFV)ʡr*JK2r::>WĞc*mCh?t0/O玟a/V|R:2E>|QOMqj#f:njH-:KNaLߥg~ p:ߨW/x@Mr5sMVr;|o(pS^K`8,iʦ% Pv]*M} dua +a67Λ&4fix1upt+t,]oI,]fq 'i_odT:ºgYw.FjeCЮnΑ֝Nyֽ ƿ%i ث0{KHZq:w`P mǿ1trx/U/ws*E}9Ѹ`@Z-L_,{ /*LK y2{E>4Glo/sNQ /;F-|B2T#[(aȟPp#/[,%Ǐ͔U5pa/&({GqchyL pnptf``))>&[b&AIGg83I,ƍ1 i19WXCGܗn N)UhT5ioY ?[&Sp^]ҙ2wcdN^im0©d*Ϊkb}'عLF(1.VN s<& JۺH.)` ATEHM$$eVNiJGEk9bFt%0N '/ Z9CI), a.ʶpuFS円W+K:NcH 'w=b8G6ك{$s矗 @`Zt;\9ꫤ1#o\1=ݱ[#•N=Y7_m^wRNߩ|ۑ|_ N?g-;y (|@{`:fv =< 3#A:{wVq")]ܸI`Z3 /ח}0i-~\Mq ~,~q m>>> $(Nmgse.5{`T\՘QԗZP_mQ_[t'r2oP쥗9 ]ӛ|spk O=Fڡ0;v`6-Y y'^5m2;7TvmڀLW̞`Ԏ!?2ҖDٔ`a0)N",td)*K$Sf4tSߑHWA{ nLt 7@|"}A~*yjDwʘQ0`._ ɎRUq3`fO.=]%OR]6u|*+Liac(7SMvS$k;P#G|wL'`*r}_vl Gis*TtffRҥC|m9qGBX]crp0&X:p):d˽-kE@ FvcI칶{I5ޛMLnɵ#k* ;H95;ߴ̼- in7VCCiVZepnb:J0*ː >&-HDV G\ϑPPl @3<8ix8dw) 3goyY7ԆcrhP {5O {@Dšx?>7-KFN 㞅y s&,"N-/,]bNzL/l/.9x꺝 ,' J?!%xCm~G0e-,膗 l:U00W1Q.Vq5i=)aSAf15wY Ab_y}rWǸ|<ڔ)ӠZaүe+_Tvfz8?}IXIg~;YO==Ff#B\TSX ;ms#^"5ՓGir568S0rvzd#ǦYgt\ؚ-ax[_Gg)0ow>Hӓ?;71m4rd fKnW֫8EϗQt8kg j|g]Gɺkp ȱd ]:Ĉy(T/ FLpgSSh3þV:k8pisz27x`I% a''ٞ,sP씷Cj3(F#eѠ4<0l$ĎiELJ-(24>#hHz D]U4hൈ7ȥS@hz$lmm;)M[_wB2ɀq=mh4D86q8_k]~Ke%)Z!nDש)cvְcƠT3{uq|l!_)cHl{o:>i&°exR~_ 7iWQ~t'3eNnj7ٙҤgSiNYVdGfؿ'J@A;#MD5#t9 z5#sэ"bx[c5#mMFhAَG0⾶18Ƌ\Rû}E2yܱ],afxqԾgt{nC/a!רS[f(a*+<:4dfpsiӨ vw l9UC9$9Ea ^S(o|67dž2f:z:@Mvͱo7r/D=8Ww _+nc= Wr |5[uctsȇm|84TG`b7k=)'b^xk06k*C0ZqcbMCȪY\F_z 6Nh׊_~֛%3FuAV'p#𚜍Iok\tK}4z\öfR ὞#1Ǵ?cG}gy_9<^af>H|:V;'I8pl,9SشXf-)e5<ƃ5ӏ5=_:M?iTIL_JN\ v=kiFY4}A~u颗 =My0~op3:2Tסnj˲Y@4&;=ѱWc7}o\C˪S󇑬w6ť )# 8 ShV؜߀MYX+Q ulAIhr t ㏊0OFЮt%崷hza7pa:ivy̌g[) p <C^G<FWMvEiЛ)X+ P'Ϟtw*l|*Ѳpkhn|UT]XMaA;a,MѼ_C+P2?t^^xׯȿnUr$xKυc5.zK ) ʰ#^4Q;-pfԭ' WZ/p>,]\bND0dv]zӅ4οGb yp"4FU.E8KIkҾBOD?THv 7Z8LlJ#DL ͛:܌]pqge]{Q2Ke O~{;E|~@?g̔їCSTMv=H w84MwmpG 逧>i7lZw7hTAtl|5Ik;>O~扖Cųv]ډnS[i6SZFd[[)si#aM VCym1d 7iGtĉrUć$0&\?|lW\[d[˟w "h( OFgT5ދ]Df-xmjnWQx>/iiWiaZ~`' V$GLl\~0V|.?-yѸ%9l#F: &T8N4q5R<@r95ky jGt^YXm?N96Jb3 :v һ1Bh#$ A1<>[#]a:Hϼ,Kq+|vf"_G 2ݖ,~K(7Yl!D=RoC6 UW;a KK[ͩ'SKn:)*,ۂpS /GUF\UC)Zh%R~}>j0K/ K떯iN@ յh4p E} zѸo#nDlm"dS}HMؔ*J*_ʪ(K-_"„:-Vi2Z'J4>#֭^, q4ÃgaA&LBmO9LAxH9-t7fgZ]jܮ\,<td9:cF#@'I#N2',sf8AMEӨ"L_7E.i  Ms;*~ThR𝰨?AemPo W^͇q?~_(YKWN!̃sȌ 0WV)-1eRjS?*?j^ uS|S^ukZ : *Y[?{Psg6BmTq`X2Ly pj߿1 ! A(IGpэ3j$D#E@X0Tp'jowʫ D lae'#6B,' ebeQ|]9*4Afe- z,{XVUn|v>~QgxTi3]V_X[` ?aVE hBs Ia1J#;CgK7E4;9)zmm pZD;tHC !/ 4ydS(g, ^( 0'LsXmL<*`~Co\^iTEr3r4x@{zK|ktYrg PJVҕÝdj6 ?p_? zs= # *{R8* U}/2y sd6S! SA G0w$81*2츋&;-BuX/F^d>OY a O0ifRvHJ?lܙPF,CRWޭN0v Jÿ)zSSJH;!*ZTFg ": D% lU0bkeq=]? m2We%^M+loܾSs^ ]𒕛mٕMzyna43 Q0lӍ[m >Ňdy8i ;Q_ a mnN;:E:݆3yT0?_HfM|9N(+Q=6HL-(%a@2 PAYvN>M˽ߋ[$xY$Yz%1L_"}>UdLtR`i -Z딹߲L7*{oW$%#z_gR_:d0 ?IV̙#OU̘u+㎲t {^ۈe ڃD/_4yfNwha/-q&(5Fr_u?.PWdho[h4x exm@k={6Vu+(JQ t9ysB .7d{.D9 gBi#@#?2 {*)2w[(͓l :%k <eFIL$mԀ :N3$.S6ϟkrlP>0lszc@urp 48G(  Ө^DoO%q5<&wMb{bYm[_si-wY]ܕK, ɓeW`#_m6Kƍewd6#t t]~_U%kP/MW FvYh~YI諗? -s($~p 0io&;j&F$gDu%r]L@TG&q+xOq wvARuF$7$Y#ZfZTK88d3)l s lD^q|]`)Oey3q+[i"&?Nt̙SڴsGՖeg˲O>e_"{dEX^̀u7{.-0pKfLO^|IjCiPK΂ 9hRN{_V/Z$Zv}J0:CfN 'k@ml6vA[gŠoa:ɿ_EKnݠN \$>)sz„Q4UoB|.P+ͺtLO'M 2_lS۷{)M:u@~.DMn:r~aZfHyчImq"ܭOOIGd7mfmֻ.w,K ݴ3wlXޖ}JfamPe+$˱4VISio/i: ߟ zͅ74f鯋wR7Y#.RTK?jgIc1THO7aYaK0EbLѯ~[Wq)cHZn: Atp_Ut/FB#*M #R:6P R 7z ɐȯpEO5_K*Q<4psMڟ r'"Wze $AM+g r'AKHcac >A r'|đ &Bg <}`.6{T?Opn.zOiO>IJ8xUUNF3԰wا.4SY>tn R|uX;&o>07r#"*Ԥ2:,&uwࢌ\2;1zϽ1IT;'㮹WI&COs< P> 50*: ڸ3j:yw}1;;PzcR:-hφ?w# lߋ/6}{\V-X "?w돹JlZpR)pD:.PEjJS r(+nF/)8$2[Iǎ2僑; r=;Nze)S[nѨ?gypCYL )z`yʹP&(8y)>Ay+u>KMȯY\}TG{hR}_ UQGGFON$(?kCخGBI؜.kpaVpd1.4=n *FdT߄ϫjMQD7EJ,GSaG3,$2‹@eMUUSR 3s߱ܬS_ur}׿}V39 bˆ}h;A#54ЩPO ;vӏSx3?>瞫S.d9K 3٠ލ[,g2?ѰU7\fYλHo-z,^"Hb5sꙝ}Nh|"Ckϖ h Id護`K"> YKER wɍ7HCL{/cyKA~y[Sc?/2{+3 a#0=qk`"&ep;|1F ӠkɡԬ]GfL ~;Dע9E^Bgq5*B!CdO%#xt<|HUm:[G,kt5LM/x#b7"Lì̀PŒ~fڀIǁ1C3Z&a|#w%8xŌAˌP\ʘ~ 屆̙C1<&󈣥gB>Yi<"KôHfI5H ġq( a.zc6iEy^__l% 23]5ߤ@64C .PObHFyiqZ_?cGQن ~mdR/Eo 1NpsDیIWJ =U:(SbL)K͙L) |)/GH* |U%5<~yg`l{xL3<8S%S1w%hxN3x(o6ra4+eSJ7\5̯b+9UoEv: فNw_m$\N,Z*O?թq?Byl?=m*߀xQXK<,lG~k4L _nѽ|4q[t&VgS7*lPu 3n~'[yx7q;=Lu?ziq~x+U9*L@ǎS!*nB,9,ނ4mK Lhg`p6aԩf4ÔH#d}3_E@Fl d7z@] "ծ:uUK?3?;GD| 8I' /Dfߊ eɇOIAc)n58Fe&o~,ɲPIaM}'hh๬2)[D!1#>'0E7l}"6Lb H ?'^M/Z&)gX㜓\%C(-6yҡ! ? V8-<&`OI+gpEWڠKyɏ 3a^h }J?m5Y_]2QbZ6 OijeըSX> d RlF|ja=9aJc&siy9 2SSFkks}?|FkdG&;4S&2=:;7w?C2-tr]]/?ϐ&\H<(sBg2O~ɮi<&A( 3ql4fA?G<Ss (McHnsZ_Yd1Ӽ7tq? bҧNя19$L`MS d[s9O_(?tʯiF zF[^N`B:헊 tš?;Vzer,xJ; ?Ӱm G& c_3lxFb$ ISlgp jّGoz#Z䏯(Oy3cpH(·_v;`sדkQ1[ο~V ]+uq.^󿑬"N)I?jT[p9beW؜fMU<T m]; %bf] zd?5>^vGȪ It$!5ZnA! =A&I[Ǭ"'b]v56fa !w^ Se`+N;q8apsjs )_羆mlXo~W) 6^sG{7|+$O]"<"Mvt fGJ럣ju57bb4Zd~d\&wQ_G i1"8GAt>ĉEӥC<| L68&@q駃Lq~,\)}vLD6`MWIg[84l d&պ2EP[L2E,$>J7 xccF%+''#xpҒ1NYCҋG!HVRFi1)#+UK8) # 4[q":餴SKXkZke5?s?ݎ9  < wp I!NgoYw3u2+ZQFbC^k$|3vwRjccB4ߴS{?`0@q w?{`C룜ۚ(vя9xað+vOƇόRܴyʹr,Eރt=v#^% 0p{%FjKjeN8i`iIғWEBjkzy,Fz p_*BGVp˱Vid=w4nܰAϜ&prG3qfTH?fan+pVzhK5g.Py~:|y4jBU ^l)M|<3-Pᾅr*t(}sɔ{Sy3hC㧼ձ ع<ʫt iiTTQ؀f:msOqO@4xl@C^7nf8*w5#p b2@M@{ =c:kͰcMk'[Ӹ4[YVI3<<͔Kf̔"4o?4΃pɂ3-*EWʬ햡)Xjݴ;A{͒%8.913cP ɉ)S$NiQoQC stjn Kq^ `S$Yl=/&r:Ϗ]>/=*јLc3 npr)N]Yud rvng1tE5F 5Qq4+0e#8$]cJ5%7}N?=lºCdkbncY6CY/ k+0\g6YD_ܼ'絤<̫GdRk1[v+ W?xIcBvs4l:7ag4y:9E߈g@8vI,Y-/ba GaE){0g쿿to;=!G\vF=z3!1NdHqYKSikFil aj*EkAm"U][?\%aLA.i])AL>QhzGY>{}Y^N@nY` )j-ǎ}^"j f;/Ly3&fxE yhDWB'.GJZ-zuqa5$9I~!ؒLc`(l_O0a f4RK j4<@*Q$:!;Mfl#*o k(r@' f>29ʞ!px)gtqX*o's /Hgyᤊw܆F=!݃v-?ߠ{NELhC˳2e./ ׄzYMBh{P$8SH+l` vI7k{ |ob4ߢoQ@ԑo>tf"͐{Rx4#5 y4r\OZiӎHp`فnybҌYWu J#Y|@7JKEg~ SI *2h; yL!MHt4vFƔ8*</qFE?}(g_H5ӤaRWsf.q-oiP9QC? 7{EmzW䌀QU&`&@/ McVrϘ{gt٦ޑqW]4|oaPFh8p|1[Nt&,Qh,8.MQt)Пu=1Bquo STU\V"-TCKZҟj T |EGL4+?ō{sU'B7?EoӫZM7eRکxYM{3M 6l̳Ȃ*6XR+nJ5 N6W39cE'/s<@%7vn#F10DMztӨÝ_ o2ne"gM˫ico~Z"~_ס=W̄xK"E`g= JSm Iw芥ϱ3ۈwH 銊ai4W,A"ؽcW=UMZ 9'|PbN CEGvڀ{WV S"lV^)`SĀjyYE3*:LhX}WeJSdQ6dI/yqOlP@4e|./{=ue,LG ]iƦA0=Ű?GbA7M:=np-קK+oT*SUQ'Xa8[$k XlؕUg~V4LGcT@ĄG N [@H,F&}VB_"7LfP`F\Yq a)݈8771?0 mҕMI2 !AWx|59 %,ͩ' .KnT2Ŗ/Zߊv)Ŕ([UcdU+5eלּ2tig60:pYEp3N0*սsڬҳ#*u(dXz44GpLTЦ 4>kǁR`M|cQKvU#2]","GcT)QSf1)o;d&iB,1PP4vӼŗMKEZOUS!n_UPjO6<ɇ`!̃SԘ, =?ID.i :kX|l!0hև[ '&BppQ]OJcB+\)t /D@ɑ4iwda9$xDc3` `uF8'Az jFQɐH&0ADS ixZm 1F\"m **[ayeKSUcߟY xcpVsxUa4qhƂC9]QZ ~^@Ry;hxu8(4:E0)RR;%Ĥ換O%#>`qQRp!^qUMɎGul Mk;pBC dٍ / H8#O@'̕UJKLYZzsǏϿ/ƆB<Wju]': HsVy0Oj/Mhp/QMBS%]a0 dDH~#3@Y7N8JH)̯F6#:q\8؊n7M5_FcrLGr*LVLol/NN5 N ? ۽!VXB7aeۜGOdM{We*}|`ߟU~&iUkʘ~BDV4whѮB^]5*(kaOa]nRa1J;?n a!,l?y]ajZW~<\mqLWj'B\3gJu46M+|7J%d]E &~E7K+IZfNGV:7uW񧺢Z_/J9AcX6{_ssY\$ʟ9o¾R?R( ʚ?g<_c6ܝ6b6˃1O1ީ`hqtj|T0p7zsϽ*$"._E+kk]/O"UNOOc|qT)M "9Su܋;CL@zLN=6j|>=x?ѹa( K9`Ӏ瘜ma_ufH1{.=|A*JK4[>"я?e|] g@|W?I|V/XRTDG\fY!D$ 46z?ihkA]>5 x. 1E#xYi/vf&EX ܶ\uq|l%%᱙VdӔ ?@)x5lQ"0 &R'3#80Nwk?FM;%#|x.JK:rduDkhnx$]%8qNy Xy9J݀qſ'F=.?^=tؤsgekoȪЙ2imG||:G4~%:wC?蠭] Ht{Mҟ/,߼nINCOtf5q-xzfh Y=3 gOi޽_0}狆YAF2gdٸvTCgY`)ҹkpoG/\^M>2EmsfgBd굻#UC?MxS`SuJ7nu(g׮UPP[j~gE^Wa̜0A6|94~uWհfYek/iұ,31Q7R4nP,lٜٲ ehWͻvW>HWݫ4N@NzIV. ̀fQݮ=QZ6 dޛScQ$D|?ŋ6چQ 6/\bs4,Q"-i0R@+%?qݯ2鐇ao19hMߌv٘4CbY,ˠ`PThs!rٞ3VE2l7 5ԾWP:1E3]ԲOKoK;le3,COu@Ul>MҮ>ҲGw4se3 c}VcVfOW^):T??*! >U;~>ߞzhp Ia{x2݂]$ms'Uɞ'sD> :披Ƹ:~&l |fyR@Hrt))A-đZ4doFX[:ʿ>M3VdC@dΓ{D]h<1Jv0@zzԬ[$AT}|߁\L{ Y53}ȿ1W\ 89G{8vԒc@IDATSQVyQV3X+j cX+fyxaҿl,y&W@uȸP[!DWCyEA9ɉ/<ȫ7ݜ۱?D83ʲl6ޏ?cJާfM$YoqgCNGFfb'cR2TmtE?GǍW9KJ0"%E@$'{]i8$YvPFy0&r'-%UA/ ܲK,=OHj%8 ̟ XLѓC_k)H&;<[+$\G:E_)zNYo^aT&$i jV@E:9`law*L3`u?ez㞃(.Lٛ8BR운QhCYB:*ϗ.MYfTQK#+̑ϻE2춛q2䃧1>=;S幛oQ}/FgO|rӰ! u5;#t ϟ(iۯ 6ViO_~YΚ[Kiמ&OIMc!XvL*1c× i7]_g>xA tcSd_ll~ |)`\­|tgd&+.biP{uV/ZdtHH]vwˉgLRetqa `pǏgMv%s\&^(P̡A|'h| 5*<ՠkb@ ZG3I#lK &.p9Y#$".ǥWGgo(`.Hxw0}QE\]()k)kNuqhҁx3. -0)l (K΃ N]i;G=B7lsE5utS督WCnӑd}>g80{#x}/#p!WQysx!F{ 0jo p.Fk/W^!k-AL4u+mӺEcES'b|6ۏ=,үעS2G=Kpɼ߂~1IwYq2c4R>O CoAK +͓LW90 @xoF]|~Z﹇e:V/y,.PڠA[2>I˴ (qƥseryM7JVɒ?vYv#q4W41 ~CXʟ <L )UBI<6%DT1VLŸ1㔧3*Ɏ=$.UrN.z49c !(35KGR b*'cr@)U~'M|x9 lm߾2e ևGez6SSceѵG0.Q{=`{v~1:%>ʫR;O4jdGM޹S/'޼1 @wRN"s_C#62[6!v?@]v+f+{stgL7SwޅC~l]:O_bSU8SVc w l!k SG==TzKcU?ӟq/p^rW10ӫ~] }N?7g ,ٴd75y3#O񌖇rB~5Wϥ ŽbR 'i=\ 3P><(\xgFqպ$D~]󵯦2B&'Mܒ-m)z&'q)xgtWc{y[{؝x?We3;q|koI+&0kJ'mg CYAUgj"̙iu=`'Wx%Z/+xkZ.yzFU OÎbopX  7a=Ws g_vON*i} [|OUXNyՕ` O󵫁|Ne43Qu5勯:/]ISO՚*u5Q(Y%*  7|jt==^ÿCÎ Q_ign]Y*pd>&ǖ}{E'脴x9M1-1$ݲ?䗼'.<u,lߗW+f8mZ: X G„aFVXW+6z+?c2v86B{5X:+[7߶{knG t>fO}:+8dw#9S@|+fm<¯oe>EOgಞO}2ʫ[jc{]IbV\lyzΧ?te'> gҴ-v=[f{.d_5/m Yl#8D9-=O78!^'`uy46x;/6"z i} _6Gj O {~nASEzڙVC]lAf|_jH xh_Wd,ħj>愴/ÎŭqJ_pַ=?1󧥛wQn"dX /}&rGC"^3xjg0Oծ䈝_2 s @đ,k镔@O59CncqInYVF%q:1:m/C1q!ߟӓ41+C ;t >F6 jYϬ@~ERUS'7╗́ $4xe'  +lG'V~b Ik2;Ċk{ >7]r nϘߏg̛O +x\}Y?I'a'P$N?oYp<3~g;=hwJ|L?4&] 'N|2^ ~ ZEOTWVWWb-r>p{plg6 moK{'7w)cb!I?ox;گ` {IڂMxFBɗJ~q8p&G,Wr0!ivĹkOoO*}qNռ!7z,7՚A~lm][.!0 ~} #p1ǧ'rm4}tėHsmt؄Vx&?#o÷j]m(=f/ h $nAcǖ6z'>`4ꦠYh?D6KoƠygNzUv([7i]{??Ī;xV=@v 6HG~K\3_ڑ7Vwঋu.8<7]-zL8Ow>xL P-.6PϹl R\mO*^pKpQzMs pGJns_,pymw|x͊#0m<uY[|f3<YMמj' Z{3ijϷ`Ej+xV' ;^6sR9bE:3~}os_ݳA&SY6jRUDzgӧXF.#-Cq "o 䋘D0מ;\r l )9*ɲJrT%hg}M(Cı[6k /ն`>n(Hx1!LTЗ-\C}7~;m%GVSJw}?})xiOJuO'\>=ޘ3f]yo9?(mwduhO{<,LW@wPhO ?>{~N|U;E?w[beC;ȷBZeixy/W0>Y|@Px.}ۡ'؊Y7|#c;|^:8!Wz+~5~60 lpc=SA{ؖ2?0/M/ϯLӖ;||%^뜂w+6L>W"&x2ox͑|ܞ.sD`:)'R|X,qusu.pߪ s3=+eUy bf:Hk:1 #-_[xcOՖz|{_#R! xI%qĄaBgMx0J,.˭*i ˆ \9ܱ[>Wٰ+cly")ل/UX9e85)_gՊ|8HF9xxxxiOkrW,nrCC7C.$hQc/x;ʑV.F7y2"6㵥S_&w{qx6Zm xQ/3e I |Nv͠6/K^!߫^Cv`ZGSWcr -<$6k~! ,6ڽr_+(iU73.1 hqq"N#GB/z}+bLb7͋vBnY3%f:2"(N` $֊:9fxBqj?#AY3Fb7x*6cq0fx=Js?唗0tL|sg'y$5LV7\twXaO |O3G7c`+*cFjӕ_Cg4}gzu 8l?.gY!7>J>0h>54Ys_yr&"|=N4ߤ~:&ӿ &0^wvkN/ǭXbAd拕m(6nUuHw`0@{tuC/UeHmt1NJx617oǍR/~p*lznk_ =_Y3 :$opż!ʻs] PNwvAo3ao dBꬳs#Mx5rCq0`66O/78ׂq<6}ꫡM7BQݏ>! ^7?ɴkuvoozj\2 3B>@5량pDvxixi}'DB9fњ|G0#b |es,z3bLI|pߣ~iRlYK G⊗48%-iHsA#0m;K茍Y'ʀģ[T7:fwtL!K?kCjb0,5e|[ǛBDžehSSxf ^ )紶 f(h^1Oir  H]_I ?)kSZ"A,x!AD`Ex;#\I2 `|.s2*iFZ>?M b>P7Tz\QYԙX%7u[~}Sc$r LP@ V9c$ZCt/:^|mg _.?^{+|R屖fO`&:cآy\-~S#J8zmiPuнn oH?zٓV ա2ky 7o>jI%;h?w-j!罼՗Ms)%3߭?}Ois+pB.RsÇD`h b?}S?oߪdLv}?߁gxVo1xAVB'6!#aBcC2, R`O.|Xc/ 7,m۞-3 ;w7iE/S Yy1 GLHő:aTNF(ʷ`ʳRc~t~8Wq#/֡@:OU^W[:̢ w1 '\bGh2\shbt!(h24pv,'#V' ^ 9'y.klǨM7ty.sI V~tY7 Nֹ4H;HNy5t[AgP@''KJ5lb_ż['y1 DY|<\* fP3XXaJڠiđLaJWx6qd'_nqiw:2[^@ 7O$!b[< l[WcB7NTy, a\˜ٹWL2Zg}e_#u?!Y9+zϼ/qFǸshǏjy|ɪ ?ˠơo(JE9 2bHa_?b%]U; lm_xe.\ؘhE&!JH;p̍Me]?y6TUͨr|a醢(&UfyiEGQ;}+j537?tj_\ teb4Dθ Q4U9阦Iɑy5Cd*0|gDplHg0""D́L( uVA2} 1#80EX_Sx|Hn1d5s#] Cqb ~|ink𸼓.p7F?!iNEkFD9輁HH.8[e^#՚ndI Nxmjr63Lk=uAtqpn[C☡ۡEn3emS064}ս6'!ҸexwLLJU m $*^K\,W]LJsP/ ҈Eޒ7r.q@̜,92|Ɏ'/*O8p6K&A CLUsXTTMH! O9:_D=Y 9>7COiľtʩϜu4PGxCZrw"|ΥAqJ9+B7la[ttYt#ҿ!)ƀz/ƊM΂c %Zokrfw?8)!!d4O2u.+WK𡛼(B[a9jV| sb7w&E I At#P\\^'hSn굨JFM߀FRQAt"LScH|T\q =P/qQOћfjG4 |ȏ<)lqa A +DZwkr>,Ca%^ucm+o +ŋnz?Ǫ-`l=I?{3g,\R5ْ",3b$:1 XĎZa-zآ_[^ZtNLrP(Gk֤h0%iuQk@~v]Emd W5okV[ŰDsX DKW6|5+EsŖ `Q?ˀt!_d$л(E{+y`0cKRpS _d_:([\N }4n3.d0yCv&xߢ3|&'Û`&.7I=}ȡǤy.tb6xQCd7Yq EG<ўW+xenČ\|]NXlj,hM 0>B0Qa ciZarlWZ S"lT'uHrIxݮwPGyπX{m+{u^ _wEtO~ϸ^F{{ǥw xx~"ʲ>3WRf>MM`/=xBY5'֯?G'Ly~n_#WvYϬhӨW/.!ˀ1x^Gflp#/SC^j?xŃkO {ԋ}0M`K.=XE/^Q >k,hO҃X@Q&'X0|W|;8axC裏vO!rO#ߔ0 gb󜮴F|OA%>O'D,0|t!S}s֡ y0 $?Cd*`.FE-rtR9x"'b Hw.2K@s7dZVIhɓȐiZ>&? 0F!B;Vbŧo8 ?SP$W -6aW&7Bac+商U0TcE˰7axC|rSH$3Ӧ=GeKer$h׽=aq0`%MMa\J m7ȯ'ZʯYLtD~ ]R`q%PoKapg@jY}U̶lL?H}N,V l[F2c&cr=xHQj(PB&\ƷH8}{G4ϳ?`I~임r(|SMC|qёVq0pX`ʨS{,EB5z kr3N3nɗڡsBaCwNԈiP Dmx[xܨ-eAMNJLѱF_>b@2a!;V3/&yNsuXQY ꘎h3krWtC9 s1#¢[E#-vsw &xuPb gBfѾF[ujdشd#1tT'g8,@IDATM0p*/ YOyZLba5jY+1BN9@Y&Dcq.S_8-zN >R6FKj镔kr  #{z/( KV(j峾Z=󸍠.#c?'݋t@PCv}mi˯0Yۘ@9ZWecj8@b"oF.T#BAl+1a kTM~2( Q?&Ǡ 8夝KbO(#"r ia>q=-y u !tTn6\`.|a| L!bPe-X*wWy 0^E~0g%<'WV7EX_˂|n9\c.D TFQ6d b+1aƸd~`,Kx!_YenQ.2O:!Z5xF,_sgscNq3N_RgB%9"mw{*c8;fFeqģ[TwdV~4]߁EmC*:R٬e/ԤZ 25, *im!71 J\P"%6<[n/zd<)6sOv{$* V[ie]/$f-kLrRjkA0 -w@]*Uee9KQr3OZW)j !:(ȫ84?1Ђo~h%h0׶?F磂CLc^}3!=0 0iʕ_rp'4!.9 öc/OѻI9-20'jƃp_ kAD%^7M=U,sYnl̖V}[bJXr] ^V0sSj]4]X„lĪ9'50+\"0T|S뭭7oyaY/һy)m(Ib ?& ĥ_ʴth/ 䈥>PG3xC7.+_,!'o/9B!?ۊP#e]og?ۨ1HpBNZ γHfIeЊBɯuMkh2|s i6E+ߍ&L,CQ7 o0'\"uM&~..cސ N\ BԀk7 o0mZ6ӡ @pk4@R1d A&x\,-dpd N˲%!S襌V~Y/b*Fg(at]sleVyޣ3oytF>j 0b gB^ዚ@Řܜ_Jmg,hf{O6yɅ9 ruDǼWOeaZACB[Ǘz2yJ^];n7dE.(ʣFH][>[õ/UE>پO~ w~J2Mq"e\(mLӤɼmr۫Q^Eg@8Ebr `]9U<^_$AoapC5df`eW!Q Յ-]u"Ey3FQɒg4ٝH 4==io>W3re o͟$lwMڢ ^YAvsN:QJ@뜌aSNe<#;$"?Z EE#%qKa/?!?CirYdhEe#V~8s} r(P{қ^ˮxh!8cEٍ]ccR?jrnR芖jm TU4Էٔ_5p cR߃Gv ^&үyl;[Ӈ>tDJ>k(3#A=i):pƙFbeE|#pwąQ˕m?|WW>a ᳷H{4}8ܘȺC!o@nuA:?oxPJW>͙I_p]( 0k(6^aB(^VF~E n?znFu#rZ#i^crrPj߾Ca˲HeU?. |`˟=./{hG(?f;Fw,-M)]|i=6D)6'Luy#9Ͽ^|5{{݇ƭӋO,]UHyA_0ߢOn/>9>u#=nIb0h&sL3rCHx9>ft?cT|3=y"6;[=aۿI NG>ts<׾v[;k~q8s俪M>~MD xYdգO LJ3 IARfy%?;2eÔ?KSk9M1#(bfoZӇ;;.=^kkw3{cro6â+c<0@B+#>UW==QۍB`6U,Xd?[m!vmt鷦~w!d\ʋݲ4s֬h"~JH'ۼ [B+ P/i0 ~|64ǥ_Ϳ+mI騣N&aEO{t4_K2?V~nzӌDyl (]c>~gѫ atKs+q:,P*{#_? 7pf ׀3g "Q,VLS7Q'6gM*4:4Ր|&y2T> =6]ұ'N1lnF'`<Ʒ胻PnLM~ }'Rt"#|RCw:8b 3my"Rx2dAX 058 =\XN|#ɧHg9! HPeyNs[#Ǚk/%W$~w.{R,}~M9_tE?wR )Ԑ8-kVN)Ķ.qJWW#EaSARFIVآeHb 7?qT;n>|4/3ɞp`)ߴtY({ 7!aaO~҆i!O@ls40tͯvd c6;EՏ!W1>@sSҖ< -x$_ntw rwMS<`,A[tJ9^͛7OU[~؏U̐dռKFQO4%NPd_Y%;d:і/c݌B Oy鎹y`oJwe|8 ;.q B>jO$gMp(M0њ<A4`6yr9gݚ<:"'pˑ;EO{Hwe]E!S9+"&ʲ2=ń0sfZI)_uO W$ߞ_#Y.lu4LVm鄴/mßX Gqq#ҏ m[j>t#͌{dԀ.3x~Ɗ5bvd\$^|cdDTёo PD>d7v4ӫaR8[၎=}]m"NEaoザ9#-s^zҕJۼ/Kst=)In2.]=(띋Uz֏;zSgqnu]v@#J-p oYI{Lqc7E[a& L.ܢ)zlU1Il ,'>Hϛ~[a6hēeGRY @ޓߤ_@6Diヲ\+^us%^x8dCXO >-Mڛ>G?'S'}X+ґGĺka&g2Q */cy31߫3x[2oy7`Ce~ׄ^~ xsL/O$ރ'c}E|wK{L _-J"tX}N> #e-z -E(Sg=Mϔ3'oӏq.(qЁ_g'?|0ӒMOg} x'"_iɞ[tQ& Up<`zY-}mx4{տy4w%`oKMf(n)^FXj3]|2.0FD|ck!o8-z YU+_]}&`t*>vவJگt\PL|yP֕%&0ָ6 rgƌ9葎֝<):// C L6:l#ޓY pǦg} {ޛfҩlmLo[9xPξLK_ꝩAIG̹1=p1T4{`~do#'xzu8dLP|p!&\jD@'1y{jj=RDU7\(x|%_=q<䎿~K믋ғÏҼ13}~kD}_Л|p RRW~>tr3T?g>E: ,/[|0N-E`gP1*'%-\Ac]3x~ +IQC|Ҿ{wc C7hw'ӻu=ެsplуh-iszS73>~Bv!'dtgwIl3 }1ViWz$ o xY9uH{=i%];*U{!-q^}\ǎ&Wȡj{tOZ̙♮loQ?bp` SȂ>gDri W 4HՕOz:YqQSp|39?qhHpCj/2gHfz٩Ր1$I Osٕ.@XDڽ 7"?Ъ֝!Ý5svҩlg7G^> H[0#aqi' p V)Poe_| +\s ĔjBtӾ?-m={ҴS9Kbaҙ+`*gA2#U&ONږo p77J>QWi+j%o&|xNTrv ԉjmɄ}7G6KK/Kw޹0]L?4t{c?$kdCEuYZʵvz=NekZPk(Vd kL]s`/u&e% [Yw Q 9mErpjD]?SH; !А.fv2a`)N, xp J!&z 6;1SM|;蓙:+!1+J ,8Nx裎=" ߱/5xap]IkrKr,u_H$#Vv%Upц0lZ׸C4̡l{2<ʘeXpmGC:$[o6)Ecj̶o-D{nKU`7Q(l`tmu՟f1c 2texYYo&TT_ŲT!㡋 eA42%#Y|}u+x7}Dɚefj#X ٸY(*GV )jYϴm>҈.S*N0)aj;R^QT,nruJs Kp a&\qIOx-Gˡ|xm?Cv4_SW\k:0 L]a {ɋ!k\N B)sNM3D} {[23w=4y:~.,U:GPgG4+ε.Z.@ ,ǡAр8>:@9X3w69jd2! ssofeM7tg.s RlNwnp2eνAen4` N4HK9ݍ&L09iY&dxI78y2 -t7 o0/Gnu"ꃿΝˆ%I.d_TReK9ȓ<1&yS ︂gQ腤0=pW2,epZ_'՟bCNf2&_QD3G{pN!p,IpRiđLaBiHɷ'3gzD sRc(KSxGQ_2| /$9kE^'zY)ktÏsb]?y #6e7o+[n6oo壮՘.:?c,x|GsTnjNnsJۄShWV%P-pP{59IJ|n3]*h&1G0h LM:!7쎸ԥwݥOɡx6y_ՐOTGz!5q+xGC~Er@4[yvW\玅^РE|&Vlc\/tV)"&kkPpr<',3S]|ьlJ8[ۂߢY3a&f &H9 l a|; ڢ 7d #!-YgB$m-C-z;3R?}1krx-&jr8miZ|J:<0>[FS wۡL9)_zZcCvM1Wuʂ_*xTk?F&uNYf~7y# !* * OW2& 5@WSDF9IrYh}~.E`1ŢlT zDŽ1o<0[Cv8-c B^Ra%^|^W~Ҭ\Q}/v\f؎|6Bu(+x*Ϥ&'O<yA:/;cp6yxBʣ6D14/MMM'L($pq`ZTHi셅7?~1C홽J G51;V@8Ga2)2"?/l_)$~dLWX`j.|(m0}K'QO?o??&qjos4^!T} ݋=,?AȟW`(#wAuqPpZ 6ȈFmWEFJiW㡓(glj FxsDoިah-\T}'570SΏ,;)aA-IYM6)N2gb=Oy󫅂A&uZh?o@"T7g~lC9{i{}v9i Eqӛ R,AgI1EX?tSFhM%QcUCj*4vipt`\K(CNjԞ׎؊hǕ=ʽ^oW\!!oq#BMЕ!tamnw?/: і H R"#R!L?Ϸa>{g|˥ΫpB8yMKJym{tՃj">+!n2m]8wJnz<ܴWѺۉ6AcPAw˿;+,\=bW>QuhCeZ 8[blzGpGy7`k>KS?~<$ ,NjQ KtYQmiA_r m ;9:(#.=6xLy@SqKuV{Rw6e(J (/oXlRY%/7q;xlϞIu"{.m1frwo bc]*dUnƽRƂ)}YnYz Rse^SJ;z[mն"%i|\ſr]):gVfk\ǿBFii٠hX{ŧ=+ ʆP:c`ɗd]HؔRԼ7GH S^ҟ5WeS=#QS6dѦk9jy44PԹS[1688ℿd_?d' lh,{eɽ`Г A&LkTj?iXx6||g]4kԃFXXJfkokF ~wޢ7J0_PW$c{edDt*O|+#;j +t-WOqy9z()Iz⡵cD:c6ik,mmPgG谍$2>d YŬv ?R!ӳ@ߗM{t6d?1 66A$pRHc_yeӞǿ7djGH )J;"[Q0vweRw3xaIIDATguow MdL?&H.އt!ɞ9V,tІz]E?]~%|DNq 9^ ~EtmL6^(X/>N_;5rpڤY#_<7M8$J_8 FPTLϳd)ZywG`1@]eh_/:z믖F̵Z+(OpTVFYopwݑJu[{gZv)N8h ?y)p7cCvl .1Ak ᔯ0); hJp7!(5q Oa?,'9ky,)P:k gpW_ܑn/$+crBFt`ĎӂCy}N ]#$}=?g'0 z6/nIENDB`tablewriter-1.0.9/cmd/000077500000000000000000000000001504170215200146055ustar00rootroot00000000000000tablewriter-1.0.9/cmd/csv2table/000077500000000000000000000000001504170215200164725ustar00rootroot00000000000000tablewriter-1.0.9/cmd/csv2table/README.md000066400000000000000000000015171504170215200177550ustar00rootroot00000000000000ASCII Table Writer Tool ========= Generate ASCII table on the fly via command line ... Installation is simple as #### Get Tool go get github.com/olekukonko/tablewriter/csv2table #### Install Tool go install github.com/olekukonko/tablewriter/csv2table #### Usage csv2table -f test.csv #### Support for Piping cat test.csv | csv2table -p=true #### Output ``` +------------+-----------+---------+ | FIRST NAME | LAST NAME | SSN | +------------+-----------+---------+ | John | Barry | 123456 | | Kathy | Smith | 687987 | | Bob | McCornick | 3979870 | +------------+-----------+---------+ ``` #### Another Piping with Header set to `false` echo dance,with,me | csv2table -p=true -h=false #### Output +-------+------+-----+ | dance | with | me | +-------+------+-----+ tablewriter-1.0.9/cmd/csv2table/_data/000077500000000000000000000000001504170215200175425ustar00rootroot00000000000000tablewriter-1.0.9/cmd/csv2table/_data/test.csv000066400000000000000000000001231504170215200212320ustar00rootroot00000000000000first_name,last_name,ssn John,Barry,123456 Kathy,Smith,687987 Bob,McCornick,3979870tablewriter-1.0.9/cmd/csv2table/_data/test_info.csv000066400000000000000000000002211504170215200222440ustar00rootroot00000000000000Field,Type,Null,Key,Default,Extra user_id,smallint(5),NO,PRI,NULL,auto_increment username,varchar(10),NO,,NULL, password,varchar(100),NO,,NULL, tablewriter-1.0.9/cmd/csv2table/csv2table.go000066400000000000000000000452401504170215200207130ustar00rootroot00000000000000package main import ( "encoding/csv" "flag" "fmt" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ts" // For terminal size "io" "math" "os" "strings" "unicode/utf8" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) var ( fileName = flag.String("f", "", "Set CSV file path (e.g., sample.csv). If empty and -p is not set, STDIN is used.") delimiter = flag.String("d", ",", "Set CSV delimiter (e.g., \",\" \"|\" \"\\t\"). For tab, use actual tab or '\\t'.") header = flag.Bool("h", true, "Treat the first row as a header.") align = flag.String("a", "none", "Set global cell alignment (none|left|right|center). 'none' uses renderer defaults.") pipe = flag.Bool("p", false, "Read CSV data from STDIN (standard input). Overrides -f if both are set.") border = flag.Bool("b", true, "Enable/disable table borders and lines (overall structure).") streaming = flag.Bool("s", false, "Enable streaming mode (processes row-by-row). Might not support all features like AutoHide.") rendererType = flag.String("renderer", "blueprint", "Set table renderer (blueprint|colorized|markdown|html|svg|ocean).") symbolStyle = flag.String("symbols", "light", "Set border symbol style (light|ascii|heavy|double|rounded|markdown|graphical|dotted|arrow|starry|hearts|tech|nature|artistic|8-bit|chaos|dots|blocks|zen|none).") rowAutoWrap = flag.String("wrap", "normal", "Set row auto-wrap mode (normal|truncate|break|none).") inferColumns = flag.Bool("infer", true, "Attempt to infer and normalize column counts if CSV rows are ragged. If false, CSV parsing errors on mismatched columns will halt.") tableMaxWidth = flag.Int("maxwidth", 0, "Set maximum table width in characters (0 for auto based on 90% terminal width or content).") debug = flag.Bool("debug", false, "Enable debug logging for tablewriter operations.") // add namespace logger = ll.Namespace("csv2table").Handler(lh.NewColorizedHandler(os.Stdout)) ) func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [options] [file]\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Reads CSV data from a file or STDIN and renders it as a formatted table.\n\n") fmt.Fprintf(os.Stderr, "If [file] is provided, it overrides the -f flag.\n") fmt.Fprintf(os.Stderr, "If no [file] and no -f is provided, and -p is not set, STDIN is used.\n\n") fmt.Fprintf(os.Stderr, "Options:\n") flag.PrintDefaults() } flag.Parse() // Handle non-flag filename argument if flag.NArg() > 0 { *fileName = flag.Arg(0) logger.Infof("Using filename from argument: %s", *fileName) } // Determine input source var inputReader io.Reader var err error if *pipe { logger.Info("Reading CSV from STDIN (pipe mode).") inputReader = os.Stdin } else if *fileName != "" { logger.Infof("Reading CSV from file: %s", *fileName) file, errFile := os.Open(*fileName) if errFile != nil { logger.Fatal("failed to open file '%s': %w", *fileName, errFile) } defer file.Close() inputReader = file } else { logger.Info("No file specified and pipe mode not active. Reading CSV from STDIN.") inputReader = os.Stdin } // Leading newline for cleaner output, unless it's HTML/SVG etc. if !isGraphicalRenderer(*rendererType) { fmt.Println() } err = process(inputReader) if err != nil { logger.Fatal(err) // process will return specific errors } if !isGraphicalRenderer(*rendererType) { fmt.Println() // Trailing newline } } func process(r io.Reader) error { // CSV Reader Configuration csvInputReader := csv.NewReader(r) if *delimiter != "" { // Handle literal \t for tab delimiter d := *delimiter if d == "\\t" { d = "\t" } runeVal, size := utf8.DecodeRuneInString(d) if size == 0 { logger.Warn("Invalid or empty delimiter specified, using default comma ','.") runeVal = ',' } csvInputReader.Comma = runeVal } // If inferring columns, we need to allow variable fields in the first pass. // If not inferring, `FieldsPerRecord = 0` will cause csv.Reader to error on inconsistent rows. if !*inferColumns { csvInputReader.FieldsPerRecord = 0 // Standard Go CSV behavior: error on inconsistent fields after first. } else { csvInputReader.FieldsPerRecord = -1 // Allow variable fields for the first pass if inferring. } // Symbol Selection var selectedSymbols tw.Symbols // (Full switch statement for symbolStyle as provided previously) switch strings.ToLower(*symbolStyle) { case "ascii": selectedSymbols = tw.NewSymbols(tw.StyleASCII) case "light", "default": selectedSymbols = tw.NewSymbols(tw.StyleLight) case "heavy": selectedSymbols = tw.NewSymbols(tw.StyleHeavy) case "double": selectedSymbols = tw.NewSymbols(tw.StyleDouble) case "rounded": selectedSymbols = tw.NewSymbols(tw.StyleRounded) case "markdown": selectedSymbols = tw.NewSymbols(tw.StyleMarkdown) case "graphical": selectedSymbols = tw.NewSymbols(tw.StyleGraphical) case "dotted": selectedSymbols = tw.NewSymbolCustom("Dotted").WithRow("·").WithColumn(":").WithTopLeft(".").WithTopMid("·").WithTopRight(".").WithMidLeft(":").WithCenter("+").WithMidRight(":").WithBottomLeft("'").WithBottomMid("·").WithBottomRight("'") case "arrow": selectedSymbols = tw.NewSymbolCustom("Arrow").WithRow("→").WithColumn("↓").WithTopLeft("↗").WithTopMid("↑").WithTopRight("↖").WithMidLeft("→").WithCenter("↔").WithMidRight("←").WithBottomLeft("↘").WithBottomMid("↓").WithBottomRight("↙") case "starry": selectedSymbols = tw.NewSymbolCustom("Starry").WithRow("★").WithColumn("☆").WithTopLeft("✧").WithTopMid("✯").WithTopRight("✧").WithMidLeft("✦").WithCenter("✶").WithMidRight("✦").WithBottomLeft("✧").WithBottomMid("✯").WithBottomRight("✧") case "hearts": selectedSymbols = tw.NewSymbolCustom("Hearts").WithRow("♥").WithColumn("❤").WithTopLeft("❥").WithTopMid("♡").WithTopRight("❥").WithMidLeft("❣").WithCenter("✚").WithMidRight("❣").WithBottomLeft("❦").WithBottomMid("♡").WithBottomRight("❦") case "tech": selectedSymbols = tw.NewSymbolCustom("Tech").WithRow("=").WithColumn("||").WithTopLeft("/*").WithTopMid("##").WithTopRight("*/").WithMidLeft("//").WithCenter("<>").WithMidRight("\\").WithBottomLeft("\\*").WithBottomMid("##").WithBottomRight("*/") case "nature": selectedSymbols = tw.NewSymbolCustom("Nature").WithRow("~").WithColumn("|").WithTopLeft("🌱").WithTopMid("🌿").WithTopRight("🌱").WithMidLeft("🍃").WithCenter("❀").WithMidRight("🍃").WithBottomLeft("🌻").WithBottomMid("🌾").WithBottomRight("🌻") case "artistic": selectedSymbols = tw.NewSymbolCustom("Artistic").WithRow("▬").WithColumn("▐").WithTopLeft("◈").WithTopMid("◊").WithTopRight("◈").WithMidLeft("◀").WithCenter("⬔").WithMidRight("▶").WithBottomLeft("◭").WithBottomMid("▣").WithBottomRight("◮") case "8-bit": selectedSymbols = tw.NewSymbolCustom("8-Bit").WithRow("■").WithColumn("█").WithTopLeft("╔").WithTopMid("▲").WithTopRight("╗").WithMidLeft("◄").WithCenter("♦").WithMidRight("►").WithBottomLeft("╚").WithBottomMid("▼").WithBottomRight("╝") case "chaos": selectedSymbols = tw.NewSymbolCustom("Chaos").WithRow("≈").WithColumn("§").WithTopLeft("⌘").WithTopMid("∞").WithTopRight("⌥").WithMidLeft("⚡").WithCenter("☯").WithMidRight("♞").WithBottomLeft("⌂").WithBottomMid("∆").WithBottomRight("◊") case "dots": selectedSymbols = tw.NewSymbolCustom("Dots").WithRow("·").WithColumn(" ").WithTopLeft("·").WithTopMid("·").WithTopRight("·").WithMidLeft(" ").WithCenter("·").WithMidRight(" ").WithBottomLeft("·").WithBottomMid("·").WithBottomRight("·") case "blocks": selectedSymbols = tw.NewSymbolCustom("Blocks").WithRow("▀").WithColumn("█").WithTopLeft("▛").WithTopMid("▀").WithTopRight("▜").WithMidLeft("▌").WithCenter("█").WithMidRight("▐").WithBottomLeft("▙").WithBottomMid("▄").WithBottomRight("▟") case "zen": selectedSymbols = tw.NewSymbolCustom("Zen").WithRow("~").WithColumn(" ").WithTopLeft(" ").WithTopMid("♨").WithTopRight(" ").WithMidLeft(" ").WithCenter("☯").WithMidRight(" ").WithBottomLeft(" ").WithBottomMid("♨").WithBottomRight(" ") case "none": selectedSymbols = tw.NewSymbols(tw.StyleNone) default: logger.Warnf("Default symbol style '%s', using default (Light).", *symbolStyle) selectedSymbols = tw.NewSymbols(tw.StyleLight) } // Base Rendition Configuration borderCfg := tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off} linesCfg := tw.Lines{ShowTop: tw.Off, ShowBottom: tw.Off, ShowHeaderLine: tw.Off, ShowFooterLine: tw.Off} separatorsCfg := tw.Separators{BetweenColumns: tw.Off, ShowHeader: tw.Off, ShowFooter: tw.Off, BetweenRows: tw.Off} if *border { borderCfg = tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On} linesCfg = tw.Lines{ShowTop: tw.On, ShowBottom: tw.On, ShowHeaderLine: tw.On, ShowFooterLine: tw.On} separatorsCfg = tw.Separators{ShowHeader: tw.On, ShowFooter: tw.On, BetweenRows: tw.Off, BetweenColumns: tw.On} } rendererConfiguredSpecifically := false switch strings.ToLower(*rendererType) { case "markdown": selectedSymbols = tw.NewSymbols(tw.StyleMarkdown) borderCfg = tw.Border{Left: tw.On, Right: tw.On, Top: tw.Off, Bottom: tw.Off} linesCfg = tw.Lines{ShowTop: tw.Off, ShowBottom: tw.Off, ShowHeaderLine: tw.On, ShowFooterLine: tw.Off} separatorsCfg = tw.Separators{BetweenColumns: tw.On, ShowHeader: tw.On, BetweenRows: tw.Off, ShowFooter: tw.Off} if !*border { borderCfg = tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off} linesCfg.ShowHeaderLine = tw.Off separatorsCfg.BetweenColumns = tw.Off } rendererConfiguredSpecifically = true case "html", "svg": // These renderers manage their own structure borderCfg = tw.Border{} linesCfg = tw.Lines{} separatorsCfg = tw.Separators{} selectedSymbols = tw.NewSymbols(tw.StyleNone) rendererConfiguredSpecifically = true } baseRendition := tw.Rendition{ Borders: borderCfg, Settings: tw.Settings{Separators: separatorsCfg, Lines: linesCfg, CompactMode: tw.Off}, Symbols: selectedSymbols, } // Renderer Instantiation var selectedRenderer tw.Renderer // For CLI, os.Stdout is the writer. For HTML/SVG, their renderers handle this. outputTarget := io.Writer(os.Stdout) // Default to os.Stdout switch strings.ToLower(*rendererType) { case "markdown": selectedRenderer = renderer.NewMarkdown(baseRendition) case "html": selectedRenderer = renderer.NewHTML(renderer.HTMLConfig{EscapeContent: true}) case "svg": selectedRenderer = renderer.NewSVG(renderer.SVGConfig{FontSize: 12, Padding: 5, Debug: *debug}) case "colorized": selectedRenderer = renderer.NewColorized() if r, ok := selectedRenderer.(tw.Renditioning); ok && !rendererConfiguredSpecifically { r.Rendition(baseRendition) } case "ocean": selectedRenderer = renderer.NewOcean() if r, ok := selectedRenderer.(tw.Renditioning); ok && !rendererConfiguredSpecifically { r.Rendition(baseRendition) } case "blueprint": fallthrough default: if *rendererType != "" && strings.ToLower(*rendererType) != "blueprint" { logger.Warnf("Default renderer type '%s', using Blueprint.", *rendererType) } selectedRenderer = renderer.NewBlueprint(baseRendition) } // Table Options & Creation calculatedMaxWidth := 0 if *tableMaxWidth > 0 { calculatedMaxWidth = *tableMaxWidth } else { termSize, err := ts.GetSize() if err == nil && termSize.Col() > 0 { calculatedMaxWidth = int(math.Floor(float64(termSize.Col()) * 0.90)) } // If termSize fails or is 0, calculatedMaxWidth remains 0 (content-based width) } if calculatedMaxWidth > 0 { logger.Infof("Calculated table max width: %d", calculatedMaxWidth) } tableOpts := []tablewriter.Option{ tablewriter.WithDebug(*debug), tablewriter.WithHeaderConfig(getHeaderConfig(*align, *rowAutoWrap)), // Can use same wrap for header or add specific tablewriter.WithRowConfig(getRowConfig(*align, *rowAutoWrap)), tablewriter.WithRenderer(selectedRenderer), tablewriter.WithTableMax(calculatedMaxWidth), // Apply max width } if *streaming { tableOpts = append(tableOpts, tablewriter.WithStreaming(tw.StreamConfig{Enable: true})) logger.Info("Streaming mode ENABLED.") } else { logger.Info("Streaming mode DISABLED (batch mode).") } table := tablewriter.NewTable(outputTarget, tableOpts...) // Data Ingestion and Normalization (Two-Pass if inferring) var headerData []string var dataRecords [][]string if *inferColumns { logger.Info("Inferring columns (two-pass CSV read).") // Pass 1: Get all records and find max columns firstPassReader := csv.NewReader(r) // r is the original io.Reader // Re-apply delimiter for firstPassReader if *delimiter != "" { d := *delimiter if d == "\\t" { d = "\t" } runeVal, size := utf8.DecodeRuneInString(d) if size == 0 { runeVal = ',' } firstPassReader.Comma = runeVal } firstPassReader.FieldsPerRecord = -1 // Allow variable fields allRawRecords, errRead := firstPassReader.ReadAll() if errRead != nil { return fmt.Errorf("error reading CSV during inference pass: %w", errRead) } if len(allRawRecords) == 0 { fmt.Println("No data to display (CSV empty or unreadable in inference pass).") return nil } maxCols := 0 if *header && len(allRawRecords) > 0 { headerData = allRawRecords[0] if len(headerData) > maxCols { maxCols = len(headerData) } if len(allRawRecords) > 1 { dataRecords = allRawRecords[1:] } } else { dataRecords = allRawRecords } for _, rec := range dataRecords { if len(rec) > maxCols { maxCols = len(rec) } } if maxCols == 0 && len(headerData) > 0 { // Only header was present maxCols = len(headerData) } logger.Infof("Inferred max columns: %d", maxCols) // Normalize header if *header && len(headerData) > 0 { normHeader := make([]string, maxCols) copy(normHeader, headerData) // Padding with empty strings is implicit if normHeader is shorter table.Header(normHeader) } // Normalize data records for i := range dataRecords { normRecord := make([]string, maxCols) copy(normRecord, dataRecords[i]) // Padding with empty strings is implicit if normRecord is shorter dataRecords[i] = normRecord } } else { logger.Info("Not inferring columns (standard CSV parsing). Errors on inconsistent rows are fatal.") // If not inferring, use the original csvInputReader (already configured) // For batch mode, ReadAll is natural. For streaming, we'll read line by line. if !*streaming { allRawRecords, errRead := csvInputReader.ReadAll() if errRead != nil { return fmt.Errorf("error reading CSV (ReadAll, no inference): %w.\nIf CSV has ragged rows, try enabling -infer flag.", errRead) } if len(allRawRecords) == 0 { fmt.Println("No data to display.") return nil } if *header && len(allRawRecords) > 0 { headerData = allRawRecords[0] table.Header(headerData) if len(allRawRecords) > 1 { dataRecords = allRawRecords[1:] } } else { dataRecords = allRawRecords } } // Streaming mode without inference will handle records directly from csvInputReader later. } // Table Population and Rendering if table.Config().Stream.Enable { logger.Info("Populating table in STREAMING mode.") if err := table.Start(); err != nil { return fmt.Errorf("error starting streaming table: %w", err) } defer table.Close() // Ensure close is called if *inferColumns { // We already have normalized headerData and dataRecords // Header already set if *header was true for i, record := range dataRecords { if err := table.Append(record); err != nil { return fmt.Errorf("error appending stream record %d (inferred): %w", i, err) } } } else { // Not inferring, read directly lineNum := 1 if *header { // Header would be the first record read by csvInputReader headerRow, errH := csvInputReader.Read() if errH != nil && errH != io.EOF { return fmt.Errorf("error reading header for streaming (no inference): %w", errH) } if errH != io.EOF && len(headerRow) > 0 { table.Header(headerRow) } lineNum = 2 } for { record, errL := csvInputReader.Read() if errL == io.EOF { break } if errL != nil { return fmt.Errorf("error reading CSV record for streaming on data line approx %d (no inference): %w", lineNum, errL) } if errA := table.Append(record); errA != nil { return fmt.Errorf("error appending stream record on data line approx %d (no inference): %w", lineNum, errA) } lineNum++ } } } else { // Batch mode logger.Info("Populating table in BATCH mode.") // If inferring, headerData and dataRecords are already prepared and normalized. // If not inferring, they were populated from ReadAll(). // Header was already set if *header was true. if err := table.Bulk(dataRecords); err != nil { return fmt.Errorf("error appending batch records: %w", err) } if err := table.Render(); err != nil { return fmt.Errorf("error rendering batch table: %w", err) } } return nil } func getHeaderConfig(alignFlag string, wrapFlag string) tw.CellConfig { cfgFmt := tw.CellFormatting{Alignment: tw.AlignCenter, AutoFormat: tw.On} switch strings.ToLower(alignFlag) { case "left": cfgFmt.Alignment = tw.AlignLeft case "right": cfgFmt.Alignment = tw.AlignRight case "center": cfgFmt.Alignment = tw.AlignCenter } switch strings.ToLower(wrapFlag) { case "truncate": cfgFmt.AutoWrap = tw.WrapTruncate case "break": cfgFmt.AutoWrap = tw.WrapBreak case "none": cfgFmt.AutoWrap = tw.WrapNone case "normal": cfgFmt.AutoWrap = tw.WrapNormal default: cfgFmt.AutoWrap = tw.WrapTruncate // Default for headers } return tw.CellConfig{ Formatting: cfgFmt, Padding: tw.CellPadding{Global: tw.Padding{Left: " ", Right: " "}}, } } func getRowConfig(alignFlag string, wrapFlag string) tw.CellConfig { cfgFmt := tw.CellFormatting{} switch strings.ToLower(alignFlag) { case "left": cfgFmt.Alignment = tw.AlignLeft case "right": cfgFmt.Alignment = tw.AlignRight case "center": cfgFmt.Alignment = tw.AlignCenter default: cfgFmt.Alignment = tw.AlignLeft } switch strings.ToLower(wrapFlag) { case "truncate": cfgFmt.AutoWrap = tw.WrapTruncate case "break": cfgFmt.AutoWrap = tw.WrapBreak case "none": cfgFmt.AutoWrap = tw.WrapNone default: cfgFmt.AutoWrap = tw.WrapNormal } return tw.CellConfig{ Formatting: cfgFmt, Padding: tw.CellPadding{Global: tw.Padding{Left: " ", Right: " "}}, } } func getFooterConfig() tw.CellConfig { // Footer doesn't currently take wrap/align flags from CLI return tw.CellConfig{ Formatting: tw.CellFormatting{Alignment: tw.AlignRight, AutoWrap: tw.WrapNormal}, } } func isGraphicalRenderer(rendererName string) bool { name := strings.ToLower(rendererName) return name == "html" || name == "svg" } tablewriter-1.0.9/config.go000066400000000000000000000660401504170215200156440ustar00rootroot00000000000000package tablewriter import ( "github.com/olekukonko/tablewriter/tw" ) // Config represents the table configuration type Config struct { MaxWidth int Header tw.CellConfig Row tw.CellConfig Footer tw.CellConfig Debug bool Stream tw.StreamConfig Behavior tw.Behavior Widths tw.CellWidth } // ConfigBuilder provides a fluent interface for building Config type ConfigBuilder struct { config Config } // NewConfigBuilder creates a new ConfigBuilder with defaults func NewConfigBuilder() *ConfigBuilder { return &ConfigBuilder{ config: defaultConfig(), } } // Build returns the built Config func (b *ConfigBuilder) Build() Config { return b.config } // Header returns a HeaderConfigBuilder for header configuration func (b *ConfigBuilder) Header() *HeaderConfigBuilder { return &HeaderConfigBuilder{ parent: b, config: &b.config.Header, } } // Row returns a RowConfigBuilder for row configuration func (b *ConfigBuilder) Row() *RowConfigBuilder { return &RowConfigBuilder{ parent: b, config: &b.config.Row, } } // Footer returns a FooterConfigBuilder for footer configuration func (b *ConfigBuilder) Footer() *FooterConfigBuilder { return &FooterConfigBuilder{ parent: b, config: &b.config.Footer, } } // Behavior returns a BehaviorConfigBuilder for behavior configuration func (b *ConfigBuilder) Behavior() *BehaviorConfigBuilder { return &BehaviorConfigBuilder{ parent: b, config: &b.config.Behavior, } } // ForColumn returns a ColumnConfigBuilder for column-specific configuration func (b *ConfigBuilder) ForColumn(col int) *ColumnConfigBuilder { return &ColumnConfigBuilder{ parent: b, col: col, } } // WithTrimSpace enables or disables automatic trimming of leading/trailing spaces. // Ignored in streaming mode. func (b *ConfigBuilder) WithTrimSpace(state tw.State) *ConfigBuilder { b.config.Behavior.TrimSpace = state return b } // WithDebug enables/disables debug logging func (b *ConfigBuilder) WithDebug(debug bool) *ConfigBuilder { b.config.Debug = debug return b } // WithAutoHide enables or disables automatic hiding of empty columns (ignored in streaming mode). func (b *ConfigBuilder) WithAutoHide(state tw.State) *ConfigBuilder { b.config.Behavior.AutoHide = state return b } // WithFooterAlignment sets the text alignment for all footer cells. // Invalid alignments are ignored. func (b *ConfigBuilder) WithFooterAlignment(align tw.Align) *ConfigBuilder { if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone { return b } b.config.Footer.Alignment.Global = align return b } // WithFooterAutoFormat enables or disables automatic formatting (e.g., title case) for footer cells. func (b *ConfigBuilder) WithFooterAutoFormat(autoFormat tw.State) *ConfigBuilder { b.config.Footer.Formatting.AutoFormat = autoFormat return b } // WithFooterAutoWrap sets the wrapping behavior for footer cells (e.g., truncate, normal, break). // Invalid wrap modes are ignored. func (b *ConfigBuilder) WithFooterAutoWrap(autoWrap int) *ConfigBuilder { if autoWrap < tw.WrapNone || autoWrap > tw.WrapBreak { return b } b.config.Footer.Formatting.AutoWrap = autoWrap return b } // WithFooterGlobalPadding sets the global padding for all footer cells. func (b *ConfigBuilder) WithFooterGlobalPadding(padding tw.Padding) *ConfigBuilder { b.config.Footer.Padding.Global = padding return b } // WithFooterMaxWidth sets the maximum content width for footer cells. // Negative values are ignored. func (b *ConfigBuilder) WithFooterMaxWidth(maxWidth int) *ConfigBuilder { if maxWidth < 0 { return b } b.config.Footer.ColMaxWidths.Global = maxWidth return b } // WithFooterMergeMode sets the merge behavior for footer cells (e.g., horizontal, hierarchical). // Invalid merge modes are ignored. func (b *ConfigBuilder) WithFooterMergeMode(mergeMode int) *ConfigBuilder { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return b } b.config.Footer.Formatting.MergeMode = mergeMode return b } // WithHeaderAlignment sets the text alignment for all header cells. // Invalid alignments are ignored. func (b *ConfigBuilder) WithHeaderAlignment(align tw.Align) *ConfigBuilder { if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone { return b } b.config.Header.Alignment.Global = align return b } // WithHeaderAutoFormat enables or disables automatic formatting (e.g., title case) for header cells. func (b *ConfigBuilder) WithHeaderAutoFormat(autoFormat tw.State) *ConfigBuilder { b.config.Header.Formatting.AutoFormat = autoFormat return b } // WithHeaderAutoWrap sets the wrapping behavior for header cells (e.g., truncate, normal). // Invalid wrap modes are ignored. func (b *ConfigBuilder) WithHeaderAutoWrap(autoWrap int) *ConfigBuilder { if autoWrap < tw.WrapNone || autoWrap > tw.WrapBreak { return b } b.config.Header.Formatting.AutoWrap = autoWrap return b } // WithHeaderGlobalPadding sets the global padding for all header cells. func (b *ConfigBuilder) WithHeaderGlobalPadding(padding tw.Padding) *ConfigBuilder { b.config.Header.Padding.Global = padding return b } // WithHeaderMaxWidth sets the maximum content width for header cells. // Negative values are ignored. func (b *ConfigBuilder) WithHeaderMaxWidth(maxWidth int) *ConfigBuilder { if maxWidth < 0 { return b } b.config.Header.ColMaxWidths.Global = maxWidth return b } // WithHeaderMergeMode sets the merge behavior for header cells (e.g., horizontal, vertical). // Invalid merge modes are ignored. func (b *ConfigBuilder) WithHeaderMergeMode(mergeMode int) *ConfigBuilder { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return b } b.config.Header.Formatting.MergeMode = mergeMode return b } // WithMaxWidth sets the maximum width for the entire table (0 means unlimited). // Negative values are treated as 0. func (b *ConfigBuilder) WithMaxWidth(width int) *ConfigBuilder { if width < 0 { b.config.MaxWidth = 0 } else { b.config.MaxWidth = width } return b } // WithRowAlignment sets the text alignment for all row cells. // Invalid alignments are ignored. func (b *ConfigBuilder) WithRowAlignment(align tw.Align) *ConfigBuilder { if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone { return b } b.config.Row.Alignment.Global = align return b } // WithRowAutoFormat enables or disables automatic formatting for row cells. func (b *ConfigBuilder) WithRowAutoFormat(autoFormat tw.State) *ConfigBuilder { b.config.Row.Formatting.AutoFormat = autoFormat return b } // WithRowAutoWrap sets the wrapping behavior for row cells (e.g., truncate, normal). // Invalid wrap modes are ignored. func (b *ConfigBuilder) WithRowAutoWrap(autoWrap int) *ConfigBuilder { if autoWrap < tw.WrapNone || autoWrap > tw.WrapBreak { return b } b.config.Row.Formatting.AutoWrap = autoWrap return b } // WithRowGlobalPadding sets the global padding for all row cells. func (b *ConfigBuilder) WithRowGlobalPadding(padding tw.Padding) *ConfigBuilder { b.config.Row.Padding.Global = padding return b } // WithRowMaxWidth sets the maximum content width for row cells. // Negative values are ignored. func (b *ConfigBuilder) WithRowMaxWidth(maxWidth int) *ConfigBuilder { if maxWidth < 0 { return b } b.config.Row.ColMaxWidths.Global = maxWidth return b } // WithRowMergeMode sets the merge behavior for row cells (e.g., horizontal, hierarchical). // Invalid merge modes are ignored. func (b *ConfigBuilder) WithRowMergeMode(mergeMode int) *ConfigBuilder { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return b } b.config.Row.Formatting.MergeMode = mergeMode return b } // HeaderConfigBuilder configures header settings type HeaderConfigBuilder struct { parent *ConfigBuilder config *tw.CellConfig } // Build returns the parent ConfigBuilder func (h *HeaderConfigBuilder) Build() *ConfigBuilder { return h.parent } // Alignment returns an AlignmentConfigBuilder for header alignment func (h *HeaderConfigBuilder) Alignment() *AlignmentConfigBuilder { return &AlignmentConfigBuilder{ parent: h.parent, config: &h.config.Alignment, section: "header", } } // Formatting returns a HeaderFormattingBuilder for header formatting func (h *HeaderConfigBuilder) Formatting() *HeaderFormattingBuilder { return &HeaderFormattingBuilder{ parent: h, config: &h.config.Formatting, section: "header", } } // Padding returns a HeaderPaddingBuilder for header padding func (h *HeaderConfigBuilder) Padding() *HeaderPaddingBuilder { return &HeaderPaddingBuilder{ parent: h, config: &h.config.Padding, section: "header", } } // Filter returns a HeaderFilterBuilder for header filtering func (h *HeaderConfigBuilder) Filter() *HeaderFilterBuilder { return &HeaderFilterBuilder{ parent: h, config: &h.config.Filter, section: "header", } } // Callbacks returns a HeaderCallbacksBuilder for header callbacks func (h *HeaderConfigBuilder) Callbacks() *HeaderCallbacksBuilder { return &HeaderCallbacksBuilder{ parent: h, config: &h.config.Callbacks, section: "header", } } // RowConfigBuilder configures row settings type RowConfigBuilder struct { parent *ConfigBuilder config *tw.CellConfig } // Build returns the parent ConfigBuilder func (r *RowConfigBuilder) Build() *ConfigBuilder { return r.parent } // Alignment returns an AlignmentConfigBuilder for row alignment func (r *RowConfigBuilder) Alignment() *AlignmentConfigBuilder { return &AlignmentConfigBuilder{ parent: r.parent, config: &r.config.Alignment, section: "row", } } // Formatting returns a RowFormattingBuilder for row formatting func (r *RowConfigBuilder) Formatting() *RowFormattingBuilder { return &RowFormattingBuilder{ parent: r, config: &r.config.Formatting, section: "row", } } // Padding returns a RowPaddingBuilder for row padding func (r *RowConfigBuilder) Padding() *RowPaddingBuilder { return &RowPaddingBuilder{ parent: r, config: &r.config.Padding, section: "row", } } // Filter returns a RowFilterBuilder for row filtering func (r *RowConfigBuilder) Filter() *RowFilterBuilder { return &RowFilterBuilder{ parent: r, config: &r.config.Filter, section: "row", } } // Callbacks returns a RowCallbacksBuilder for row callbacks func (r *RowConfigBuilder) Callbacks() *RowCallbacksBuilder { return &RowCallbacksBuilder{ parent: r, config: &r.config.Callbacks, section: "row", } } // FooterConfigBuilder configures footer settings type FooterConfigBuilder struct { parent *ConfigBuilder config *tw.CellConfig } // Build returns the parent ConfigBuilder func (f *FooterConfigBuilder) Build() *ConfigBuilder { return f.parent } // Alignment returns an AlignmentConfigBuilder for footer alignment func (f *FooterConfigBuilder) Alignment() *AlignmentConfigBuilder { return &AlignmentConfigBuilder{ parent: f.parent, config: &f.config.Alignment, section: "footer", } } // Formatting returns a FooterFormattingBuilder for footer formatting func (f *FooterConfigBuilder) Formatting() *FooterFormattingBuilder { return &FooterFormattingBuilder{ parent: f, config: &f.config.Formatting, section: "footer", } } // Padding returns a FooterPaddingBuilder for footer padding func (f *FooterConfigBuilder) Padding() *FooterPaddingBuilder { return &FooterPaddingBuilder{ parent: f, config: &f.config.Padding, section: "footer", } } // Filter returns a FooterFilterBuilder for footer filtering func (f *FooterConfigBuilder) Filter() *FooterFilterBuilder { return &FooterFilterBuilder{ parent: f, config: &f.config.Filter, section: "footer", } } // Callbacks returns a FooterCallbacksBuilder for footer callbacks func (f *FooterConfigBuilder) Callbacks() *FooterCallbacksBuilder { return &FooterCallbacksBuilder{ parent: f, config: &f.config.Callbacks, section: "footer", } } // AlignmentConfigBuilder configures alignment settings type AlignmentConfigBuilder struct { parent *ConfigBuilder config *tw.CellAlignment section string } // Build returns the parent ConfigBuilder func (a *AlignmentConfigBuilder) Build() *ConfigBuilder { return a.parent } // WithGlobal sets global alignment func (a *AlignmentConfigBuilder) WithGlobal(align tw.Align) *AlignmentConfigBuilder { if err := align.Validate(); err == nil { a.config.Global = align } return a } // WithPerColumn sets per-column alignments func (a *AlignmentConfigBuilder) WithPerColumn(alignments []tw.Align) *AlignmentConfigBuilder { if len(alignments) > 0 { a.config.PerColumn = alignments } return a } // HeaderFormattingBuilder configures header formatting type HeaderFormattingBuilder struct { parent *HeaderConfigBuilder config *tw.CellFormatting section string } // Build returns the parent HeaderConfigBuilder func (hf *HeaderFormattingBuilder) Build() *HeaderConfigBuilder { return hf.parent } // WithAutoFormat enables/disables auto formatting func (hf *HeaderFormattingBuilder) WithAutoFormat(autoFormat tw.State) *HeaderFormattingBuilder { hf.config.AutoFormat = autoFormat return hf } // WithAutoWrap sets auto wrap mode func (hf *HeaderFormattingBuilder) WithAutoWrap(autoWrap int) *HeaderFormattingBuilder { if autoWrap >= tw.WrapNone && autoWrap <= tw.WrapBreak { hf.config.AutoWrap = autoWrap } return hf } // WithMergeMode sets merge mode func (hf *HeaderFormattingBuilder) WithMergeMode(mergeMode int) *HeaderFormattingBuilder { if mergeMode >= tw.MergeNone && mergeMode <= tw.MergeHierarchical { hf.config.MergeMode = mergeMode } return hf } // RowFormattingBuilder configures row formatting type RowFormattingBuilder struct { parent *RowConfigBuilder config *tw.CellFormatting section string } // Build returns the parent RowConfigBuilder func (rf *RowFormattingBuilder) Build() *RowConfigBuilder { return rf.parent } // WithAutoFormat enables/disables auto formatting func (rf *RowFormattingBuilder) WithAutoFormat(autoFormat tw.State) *RowFormattingBuilder { rf.config.AutoFormat = autoFormat return rf } // WithAutoWrap sets auto wrap mode func (rf *RowFormattingBuilder) WithAutoWrap(autoWrap int) *RowFormattingBuilder { if autoWrap >= tw.WrapNone && autoWrap <= tw.WrapBreak { rf.config.AutoWrap = autoWrap } return rf } // WithMergeMode sets merge mode func (rf *RowFormattingBuilder) WithMergeMode(mergeMode int) *RowFormattingBuilder { if mergeMode >= tw.MergeNone && mergeMode <= tw.MergeHierarchical { rf.config.MergeMode = mergeMode } return rf } // FooterFormattingBuilder configures footer formatting type FooterFormattingBuilder struct { parent *FooterConfigBuilder config *tw.CellFormatting section string } // Build returns the parent FooterConfigBuilder func (ff *FooterFormattingBuilder) Build() *FooterConfigBuilder { return ff.parent } // WithAutoFormat enables/disables auto formatting func (ff *FooterFormattingBuilder) WithAutoFormat(autoFormat tw.State) *FooterFormattingBuilder { ff.config.AutoFormat = autoFormat return ff } // WithAutoWrap sets auto wrap mode func (ff *FooterFormattingBuilder) WithAutoWrap(autoWrap int) *FooterFormattingBuilder { if autoWrap >= tw.WrapNone && autoWrap <= tw.WrapBreak { ff.config.AutoWrap = autoWrap } return ff } // WithMergeMode sets merge mode func (ff *FooterFormattingBuilder) WithMergeMode(mergeMode int) *FooterFormattingBuilder { if mergeMode >= tw.MergeNone && mergeMode <= tw.MergeHierarchical { ff.config.MergeMode = mergeMode } return ff } // HeaderPaddingBuilder configures header padding type HeaderPaddingBuilder struct { parent *HeaderConfigBuilder config *tw.CellPadding section string } // Build returns the parent HeaderConfigBuilder func (hp *HeaderPaddingBuilder) Build() *HeaderConfigBuilder { return hp.parent } // WithGlobal sets global padding func (hp *HeaderPaddingBuilder) WithGlobal(padding tw.Padding) *HeaderPaddingBuilder { hp.config.Global = padding return hp } // WithPerColumn sets per-column padding func (hp *HeaderPaddingBuilder) WithPerColumn(padding []tw.Padding) *HeaderPaddingBuilder { hp.config.PerColumn = padding return hp } // AddColumnPadding adds padding for a specific column in the header func (hp *HeaderPaddingBuilder) AddColumnPadding(padding tw.Padding) *HeaderPaddingBuilder { hp.config.PerColumn = append(hp.config.PerColumn, padding) return hp } // RowPaddingBuilder configures row padding type RowPaddingBuilder struct { parent *RowConfigBuilder config *tw.CellPadding section string } // Build returns the parent RowConfigBuilder func (rp *RowPaddingBuilder) Build() *RowConfigBuilder { return rp.parent } // WithGlobal sets global padding func (rp *RowPaddingBuilder) WithGlobal(padding tw.Padding) *RowPaddingBuilder { rp.config.Global = padding return rp } // WithPerColumn sets per-column padding func (rp *RowPaddingBuilder) WithPerColumn(padding []tw.Padding) *RowPaddingBuilder { rp.config.PerColumn = padding return rp } // AddColumnPadding adds padding for a specific column in the rows func (rp *RowPaddingBuilder) AddColumnPadding(padding tw.Padding) *RowPaddingBuilder { rp.config.PerColumn = append(rp.config.PerColumn, padding) return rp } // FooterPaddingBuilder configures footer padding type FooterPaddingBuilder struct { parent *FooterConfigBuilder config *tw.CellPadding section string } // Build returns the parent FooterConfigBuilder func (fp *FooterPaddingBuilder) Build() *FooterConfigBuilder { return fp.parent } // WithGlobal sets global padding func (fp *FooterPaddingBuilder) WithGlobal(padding tw.Padding) *FooterPaddingBuilder { fp.config.Global = padding return fp } // WithPerColumn sets per-column padding func (fp *FooterPaddingBuilder) WithPerColumn(padding []tw.Padding) *FooterPaddingBuilder { fp.config.PerColumn = padding return fp } // AddColumnPadding adds padding for a specific column in the footer func (fp *FooterPaddingBuilder) AddColumnPadding(padding tw.Padding) *FooterPaddingBuilder { fp.config.PerColumn = append(fp.config.PerColumn, padding) return fp } // BehaviorConfigBuilder configures behavior settings type BehaviorConfigBuilder struct { parent *ConfigBuilder config *tw.Behavior } // Build returns the parent ConfigBuilder func (bb *BehaviorConfigBuilder) Build() *ConfigBuilder { return bb.parent } // WithAutoHide enables/disables auto-hide func (bb *BehaviorConfigBuilder) WithAutoHide(state tw.State) *BehaviorConfigBuilder { bb.config.AutoHide = state return bb } // WithTrimSpace enables/disables trim space func (bb *BehaviorConfigBuilder) WithTrimSpace(state tw.State) *BehaviorConfigBuilder { bb.config.TrimSpace = state return bb } // WithHeaderHide enables/disables header visibility func (bb *BehaviorConfigBuilder) WithHeaderHide(state tw.State) *BehaviorConfigBuilder { bb.config.Header.Hide = state return bb } // WithFooterHide enables/disables footer visibility func (bb *BehaviorConfigBuilder) WithFooterHide(state tw.State) *BehaviorConfigBuilder { bb.config.Footer.Hide = state return bb } // WithCompactMerge enables/disables compact width optimization for merged cells func (bb *BehaviorConfigBuilder) WithCompactMerge(state tw.State) *BehaviorConfigBuilder { bb.config.Compact.Merge = state return bb } // WithAutoHeader enables/disables automatic header extraction for structs in Bulk. func (bb *BehaviorConfigBuilder) WithAutoHeader(state tw.State) *BehaviorConfigBuilder { bb.config.Structs.AutoHeader = state return bb } // ColumnConfigBuilder configures column-specific settings type ColumnConfigBuilder struct { parent *ConfigBuilder col int } // Build returns the parent ConfigBuilder func (c *ColumnConfigBuilder) Build() *ConfigBuilder { return c.parent } // WithAlignment sets alignment for the column func (c *ColumnConfigBuilder) WithAlignment(align tw.Align) *ColumnConfigBuilder { if err := align.Validate(); err == nil { // Ensure slices are large enough if len(c.parent.config.Header.Alignment.PerColumn) <= c.col { newAligns := make([]tw.Align, c.col+1) copy(newAligns, c.parent.config.Header.Alignment.PerColumn) c.parent.config.Header.Alignment.PerColumn = newAligns } c.parent.config.Header.Alignment.PerColumn[c.col] = align if len(c.parent.config.Row.Alignment.PerColumn) <= c.col { newAligns := make([]tw.Align, c.col+1) copy(newAligns, c.parent.config.Row.Alignment.PerColumn) c.parent.config.Row.Alignment.PerColumn = newAligns } c.parent.config.Row.Alignment.PerColumn[c.col] = align if len(c.parent.config.Footer.Alignment.PerColumn) <= c.col { newAligns := make([]tw.Align, c.col+1) copy(newAligns, c.parent.config.Footer.Alignment.PerColumn) c.parent.config.Footer.Alignment.PerColumn = newAligns } c.parent.config.Footer.Alignment.PerColumn[c.col] = align } return c } // WithMaxWidth sets max width for the column func (c *ColumnConfigBuilder) WithMaxWidth(width int) *ColumnConfigBuilder { if width >= 0 { // Initialize maps if needed if c.parent.config.Header.ColMaxWidths.PerColumn == nil { c.parent.config.Header.ColMaxWidths.PerColumn = make(tw.Mapper[int, int]) c.parent.config.Row.ColMaxWidths.PerColumn = make(tw.Mapper[int, int]) c.parent.config.Footer.ColMaxWidths.PerColumn = make(tw.Mapper[int, int]) } c.parent.config.Header.ColMaxWidths.PerColumn[c.col] = width c.parent.config.Row.ColMaxWidths.PerColumn[c.col] = width c.parent.config.Footer.ColMaxWidths.PerColumn[c.col] = width } return c } // HeaderFilterBuilder configures header filtering type HeaderFilterBuilder struct { parent *HeaderConfigBuilder config *tw.CellFilter section string } // Build returns the parent HeaderConfigBuilder func (hf *HeaderFilterBuilder) Build() *HeaderConfigBuilder { return hf.parent } // WithGlobal sets the global filter function for the header func (hf *HeaderFilterBuilder) WithGlobal(filter func([]string) []string) *HeaderFilterBuilder { if filter != nil { hf.config.Global = filter } return hf } // WithPerColumn sets per-column filter functions for the header func (hf *HeaderFilterBuilder) WithPerColumn(filters []func(string) string) *HeaderFilterBuilder { if len(filters) > 0 { hf.config.PerColumn = filters } return hf } // AddColumnFilter adds a filter function for a specific column in the header func (hf *HeaderFilterBuilder) AddColumnFilter(filter func(string) string) *HeaderFilterBuilder { if filter != nil { hf.config.PerColumn = append(hf.config.PerColumn, filter) } return hf } // RowFilterBuilder configures row filtering type RowFilterBuilder struct { parent *RowConfigBuilder config *tw.CellFilter section string } // Build returns the parent RowConfigBuilder func (rf *RowFilterBuilder) Build() *RowConfigBuilder { return rf.parent } // WithGlobal sets the global filter function for the rows func (rf *RowFilterBuilder) WithGlobal(filter func([]string) []string) *RowFilterBuilder { if filter != nil { rf.config.Global = filter } return rf } // WithPerColumn sets per-column filter functions for the rows func (rf *RowFilterBuilder) WithPerColumn(filters []func(string) string) *RowFilterBuilder { if len(filters) > 0 { rf.config.PerColumn = filters } return rf } // AddColumnFilter adds a filter function for a specific column in the rows func (rf *RowFilterBuilder) AddColumnFilter(filter func(string) string) *RowFilterBuilder { if filter != nil { rf.config.PerColumn = append(rf.config.PerColumn, filter) } return rf } // FooterFilterBuilder configures footer filtering type FooterFilterBuilder struct { parent *FooterConfigBuilder config *tw.CellFilter section string } // Build returns the parent FooterConfigBuilder func (ff *FooterFilterBuilder) Build() *FooterConfigBuilder { return ff.parent } // WithGlobal sets the global filter function for the footer func (ff *FooterFilterBuilder) WithGlobal(filter func([]string) []string) *FooterFilterBuilder { if filter != nil { ff.config.Global = filter } return ff } // WithPerColumn sets per-column filter functions for the footer func (ff *FooterFilterBuilder) WithPerColumn(filters []func(string) string) *FooterFilterBuilder { if len(filters) > 0 { ff.config.PerColumn = filters } return ff } // AddColumnFilter adds a filter function for a specific column in the footer func (ff *FooterFilterBuilder) AddColumnFilter(filter func(string) string) *FooterFilterBuilder { if filter != nil { ff.config.PerColumn = append(ff.config.PerColumn, filter) } return ff } // HeaderCallbacksBuilder configures header callbacks type HeaderCallbacksBuilder struct { parent *HeaderConfigBuilder config *tw.CellCallbacks section string } // Build returns the parent HeaderConfigBuilder func (hc *HeaderCallbacksBuilder) Build() *HeaderConfigBuilder { return hc.parent } // WithGlobal sets the global callback function for the header func (hc *HeaderCallbacksBuilder) WithGlobal(callback func()) *HeaderCallbacksBuilder { if callback != nil { hc.config.Global = callback } return hc } // WithPerColumn sets per-column callback functions for the header func (hc *HeaderCallbacksBuilder) WithPerColumn(callbacks []func()) *HeaderCallbacksBuilder { if len(callbacks) > 0 { hc.config.PerColumn = callbacks } return hc } // AddColumnCallback adds a callback function for a specific column in the header func (hc *HeaderCallbacksBuilder) AddColumnCallback(callback func()) *HeaderCallbacksBuilder { if callback != nil { hc.config.PerColumn = append(hc.config.PerColumn, callback) } return hc } // RowCallbacksBuilder configures row callbacks type RowCallbacksBuilder struct { parent *RowConfigBuilder config *tw.CellCallbacks section string } // Build returns the parent RowConfigBuilder func (rc *RowCallbacksBuilder) Build() *RowConfigBuilder { return rc.parent } // WithGlobal sets the global callback function for the rows func (rc *RowCallbacksBuilder) WithGlobal(callback func()) *RowCallbacksBuilder { if callback != nil { rc.config.Global = callback } return rc } // WithPerColumn sets per-column callback functions for the rows func (rc *RowCallbacksBuilder) WithPerColumn(callbacks []func()) *RowCallbacksBuilder { if len(callbacks) > 0 { rc.config.PerColumn = callbacks } return rc } // AddColumnCallback adds a callback function for a specific column in the rows func (rc *RowCallbacksBuilder) AddColumnCallback(callback func()) *RowCallbacksBuilder { if callback != nil { rc.config.PerColumn = append(rc.config.PerColumn, callback) } return rc } // FooterCallbacksBuilder configures footer callbacks type FooterCallbacksBuilder struct { parent *FooterConfigBuilder config *tw.CellCallbacks section string } // Build returns the parent FooterConfigBuilder func (fc *FooterCallbacksBuilder) Build() *FooterConfigBuilder { return fc.parent } // WithGlobal sets the global callback function for the footer func (fc *FooterCallbacksBuilder) WithGlobal(callback func()) *FooterCallbacksBuilder { if callback != nil { fc.config.Global = callback } return fc } // WithPerColumn sets per-column callback functions for the footer func (fc *FooterCallbacksBuilder) WithPerColumn(callbacks []func()) *FooterCallbacksBuilder { if len(callbacks) > 0 { fc.config.PerColumn = callbacks } return fc } // AddColumnCallback adds a callback function for a specific column in the footer func (fc *FooterCallbacksBuilder) AddColumnCallback(callback func()) *FooterCallbacksBuilder { if callback != nil { fc.config.PerColumn = append(fc.config.PerColumn, callback) } return fc } tablewriter-1.0.9/csv.go000066400000000000000000000063561504170215200151760ustar00rootroot00000000000000package tablewriter import ( "encoding/csv" "io" "os" ) // NewCSV Start A new table by importing from a CSV file // Takes io.Writer and csv File name func NewCSV(writer io.Writer, fileName string, hasHeader bool, opts ...Option) (*Table, error) { // Open the CSV file file, err := os.Open(fileName) if err != nil { // Log implicitly handled by NewTable if logger is configured via opts return nil, err // Return nil *Table on error } defer file.Close() // Ensure file is closed // Create a CSV reader csvReader := csv.NewReader(file) // Delegate to NewCSVReader, passing through the options return NewCSVReader(writer, csvReader, hasHeader, opts...) } // NewCSVReader Start a New Table Writer with csv.Reader // This enables customisation such as reader.Comma = ';' // See http://golang.org/src/pkg/encoding/csv/reader.go?s=3213:3671#L94 func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool, opts ...Option) (*Table, error) { // Create a new table instance using the modern API and provided options. // Options configure the table's appearance and behavior (renderer, borders, etc.). t := NewTable(writer, opts...) // Logger setup happens here if WithLogger/WithDebug is passed // Process header row if specified if hasHeader { headers, err := csvReader.Read() if err != nil { // Handle EOF specifically: means the CSV was empty or contained only an empty header line. if err == io.EOF { t.logger.Debug("NewCSVReader: CSV empty or only header found (EOF after header read attempt).") // Return the table configured by opts, but without data/header. // It's ready for Render() which will likely output nothing or just borders if configured. return t, nil } // Log other read errors t.logger.Errorf("NewCSVReader: Error reading CSV header: %v", err) return nil, err // Return nil *Table on critical read error } // Check if the read header is genuinely empty (e.g., a blank line in the CSV) isEmptyHeader := true for _, h := range headers { if h != "" { isEmptyHeader = false break } } if !isEmptyHeader { t.Header(headers) // Use the Table method to set the header data t.logger.Debugf("NewCSVReader: Header set from CSV: %v", headers) } else { t.logger.Debug("NewCSVReader: Read an empty header line, skipping setting table header.") } } // Process data rows rowCount := 0 for { record, err := csvReader.Read() if err == io.EOF { break // Reached the end of the CSV data } if err != nil { // Log other read errors during data processing t.logger.Errorf("NewCSVReader: Error reading CSV record: %v", err) return nil, err // Return nil *Table on critical read error } // Append the record to the table's internal buffer (for batch rendering). // The Table.Append method handles conversion and storage. if appendErr := t.Append(record); appendErr != nil { t.logger.Errorf("NewCSVReader: Error appending record #%d: %v", rowCount+1, appendErr) // Decide if append error is fatal. For now, let's treat it as fatal. return nil, appendErr } rowCount++ } t.logger.Debugf("NewCSVReader: Finished reading CSV. Appended %d data rows.", rowCount) // Return the configured and populated table instance, ready for Render() call. return t, nil } tablewriter-1.0.9/deprecated.go000066400000000000000000000200471504170215200164740ustar00rootroot00000000000000package tablewriter import ( "github.com/olekukonko/tablewriter/tw" ) // WithBorders configures the table's border settings by updating the renderer's border configuration. // This function is deprecated and will be removed in a future version. // // Deprecated: Use [WithRendition] to configure border settings for renderers that support // [tw.Renditioning], or update the renderer's [tw.RenderConfig] directly via its Config() method. // This function has no effect if no renderer is set on the table. // // Example migration: // // // Old (deprecated) // table.Options(WithBorders(tw.Border{Top: true, Bottom: true})) // // New (recommended) // table.Options(WithRendition(tw.Rendition{Borders: tw.Border{Top: true, Bottom: true}})) // // Parameters: // - borders: The [tw.Border] configuration to apply to the renderer's borders. // // Returns: // // An [Option] that updates the renderer's border settings if a renderer is set. // Logs a debug message if debugging is enabled and a renderer is present. func WithBorders(borders tw.Border) Option { return func(target *Table) { if target.renderer != nil { cfg := target.renderer.Config() cfg.Borders = borders if target.logger != nil { target.logger.Debugf("Option: WithBorders applied to Table: %+v", borders) } } } } // Behavior is an alias for [tw.Behavior] to configure table behavior settings. // This type is deprecated and will be removed in a future version. // // Deprecated: Use [tw.Behavior] directly to configure settings such as auto-hiding empty // columns, trimming spaces, or controlling header/footer visibility. // // Example migration: // // // Old (deprecated) // var b tablewriter.Behavior = tablewriter.Behavior{AutoHide: tw.On} // // New (recommended) // var b tw.Behavior = tw.Behavior{AutoHide: tw.On} type Behavior tw.Behavior // Settings is an alias for [tw.Settings] to configure renderer settings. // This type is deprecated and will be removed in a future version. // // Deprecated: Use [tw.Settings] directly to configure renderer settings, such as // separators and line styles. // // Example migration: // // // Old (deprecated) // var s tablewriter.Settings = tablewriter.Settings{Separator: "|"} // // New (recommended) // var s tw.Settings = tw.Settings{Separator: "|"} type Settings tw.Settings // WithRendererSettings updates the renderer's settings, such as separators and line styles. // This function is deprecated and will be removed in a future version. // // Deprecated: Use [WithRendition] to update renderer settings for renderers that implement // [tw.Renditioning], or configure the renderer's [tw.Settings] directly via its // [tw.Renderer.Config] method. This function has no effect if no renderer is set. // // Example migration: // // // Old (deprecated) // table.Options(WithRendererSettings(tw.Settings{Separator: "|"})) // // New (recommended) // table.Options(WithRendition(tw.Rendition{Settings: tw.Settings{Separator: "|"}})) // // Parameters: // - settings: The [tw.Settings] configuration to apply to the renderer. // // Returns: // // An [Option] that updates the renderer's settings if a renderer is set. // Logs a debug message if debugging is enabled and a renderer is present. func WithRendererSettings(settings tw.Settings) Option { return func(target *Table) { if target.renderer != nil { cfg := target.renderer.Config() cfg.Settings = settings if target.logger != nil { target.logger.Debugf("Option: WithRendererSettings applied to Table: %+v", settings) } } } } // WithAlignment sets the text alignment for footer cells within the formatting configuration. // This method is deprecated and will be removed in the next version. // // Deprecated: Use [FooterConfigBuilder.Alignment] with [AlignmentConfigBuilder.WithGlobal] // or [AlignmentConfigBuilder.WithPerColumn] to configure footer alignments. // Alternatively, apply a complete [tw.CellAlignment] configuration using // [WithFooterAlignmentConfig]. // // Example migration: // // // Old (deprecated) // builder.Footer().Formatting().WithAlignment(tw.AlignRight) // // New (recommended) // builder.Footer().Alignment().WithGlobal(tw.AlignRight) // // Or // table.Options(WithFooterAlignmentConfig(tw.CellAlignment{Global: tw.AlignRight})) // // Parameters: // - align: The [tw.Align] value to set for footer cells. Valid values are // [tw.AlignLeft], [tw.AlignRight], [tw.AlignCenter], and [tw.AlignNone]. // Invalid alignments are ignored. // // Returns: // // The [FooterFormattingBuilder] instance for method chaining. func (ff *FooterFormattingBuilder) WithAlignment(align tw.Align) *FooterFormattingBuilder { if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone { return ff } ff.config.Alignment = align return ff } // WithAlignment sets the text alignment for header cells within the formatting configuration. // This method is deprecated and will be removed in the next version. // // Deprecated: Use [HeaderConfigBuilder.Alignment] with [AlignmentConfigBuilder.WithGlobal] // or [AlignmentConfigBuilder.WithPerColumn] to configure header alignments. // Alternatively, apply a complete [tw.CellAlignment] configuration using // [WithHeaderAlignmentConfig]. // // Example migration: // // // Old (deprecated) // builder.Header().Formatting().WithAlignment(tw.AlignCenter) // // New (recommended) // builder.Header().Alignment().WithGlobal(tw.AlignCenter) // // Or // table.Options(WithHeaderAlignmentConfig(tw.CellAlignment{Global: tw.AlignCenter})) // // Parameters: // - align: The [tw.Align] value to set for header cells. Valid values are // [tw.AlignLeft], [tw.AlignRight], [tw.AlignCenter], and [tw.AlignNone]. // Invalid alignments are ignored. // // Returns: // // The [HeaderFormattingBuilder] instance for method chaining. func (hf *HeaderFormattingBuilder) WithAlignment(align tw.Align) *HeaderFormattingBuilder { if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone { return hf } hf.config.Alignment = align return hf } // WithAlignment sets the text alignment for row cells within the formatting configuration. // This method is deprecated and will be removed in the next version. // // Deprecated: Use [RowConfigBuilder.Alignment] with [AlignmentConfigBuilder.WithGlobal] // or [AlignmentConfigBuilder.WithPerColumn] to configure row alignments. // Alternatively, apply a complete [tw.CellAlignment] configuration using // [WithRowAlignmentConfig]. // // Example migration: // // // Old (deprecated) // builder.Row().Formatting().WithAlignment(tw.AlignLeft) // // New (recommended) // builder.Row().Alignment().WithGlobal(tw.AlignLeft) // // Or // table.Options(WithRowAlignmentConfig(tw.CellAlignment{Global: tw.AlignLeft})) // // Parameters: // - align: The [tw.Align] value to set for row cells. Valid values are // [tw.AlignLeft], [tw.AlignRight], [tw.AlignCenter], and [tw.AlignNone]. // Invalid alignments are ignored. // // Returns: // // The [RowFormattingBuilder] instance for method chaining. func (rf *RowFormattingBuilder) WithAlignment(align tw.Align) *RowFormattingBuilder { if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone { return rf } rf.config.Alignment = align return rf } // WithTableMax sets the maximum width of the entire table in characters. // Negative values are ignored, and the change is logged if debugging is enabled. // The width constrains the table's rendering, potentially causing text wrapping or truncation // based on the configuration's wrapping settings (e.g., tw.WrapTruncate). // If debug logging is enabled via WithDebug(true), the applied width is logged. // // Deprecated: Use WithMaxWidth instead, which provides the same functionality with a clearer name // and consistent naming across the package. For example: // // tablewriter.NewTable(os.Stdout, tablewriter.WithMaxWidth(80)) func WithTableMax(width int) Option { return func(target *Table) { if width < 0 { return } target.config.MaxWidth = width if target.logger != nil { target.logger.Debugf("Option: WithTableMax applied to Table: %v", width) } } } tablewriter-1.0.9/go.mod000066400000000000000000000006761504170215200151610ustar00rootroot00000000000000module github.com/olekukonko/tablewriter go 1.21 require ( github.com/fatih/color v1.15.0 github.com/mattn/go-runewidth v0.0.16 github.com/olekukonko/errors v1.1.0 github.com/olekukonko/ll v0.0.9 github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 github.com/rivo/uniseg v0.2.0 ) require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect golang.org/x/sys v0.12.0 // indirect ) tablewriter-1.0.9/go.sum000066400000000000000000000034751504170215200152060ustar00rootroot00000000000000github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= tablewriter-1.0.9/option.go000066400000000000000000000703351504170215200157110ustar00rootroot00000000000000package tablewriter import ( "github.com/mattn/go-runewidth" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" "reflect" ) // Option defines a function type for configuring a Table instance. type Option func(target *Table) // WithAutoHide enables or disables automatic hiding of columns with empty data rows. // Logs the change if debugging is enabled. func WithAutoHide(state tw.State) Option { return func(target *Table) { target.config.Behavior.AutoHide = state if target.logger != nil { target.logger.Debugf("Option: WithAutoHide applied to Table: %v", state) } } } // WithColumnMax sets a global maximum column width for the table in streaming mode. // Negative values are ignored, and the change is logged if debugging is enabled. func WithColumnMax(width int) Option { return func(target *Table) { if width < 0 { return } target.config.Widths.Global = width if target.logger != nil { target.logger.Debugf("Option: WithColumnMax applied to Table: %v", width) } } } // WithMaxWidth sets a global maximum table width for the table. // Negative values are ignored, and the change is logged if debugging is enabled. func WithMaxWidth(width int) Option { return func(target *Table) { if width < 0 { return } target.config.MaxWidth = width if target.logger != nil { target.logger.Debugf("Option: WithTableMax applied to Table: %v", width) } } } // WithWidths sets per-column widths for the table. // Negative widths are removed, and the change is logged if debugging is enabled. func WithWidths(width tw.CellWidth) Option { return func(target *Table) { target.config.Widths = width if target.logger != nil { target.logger.Debugf("Option: WithColumnWidths applied to Table: %v", width) } } } // WithColumnWidths sets per-column widths for the table. // Negative widths are removed, and the change is logged if debugging is enabled. func WithColumnWidths(widths tw.Mapper[int, int]) Option { return func(target *Table) { for k, v := range widths { if v < 0 { delete(widths, k) } } target.config.Widths.PerColumn = widths if target.logger != nil { target.logger.Debugf("Option: WithColumnWidths applied to Table: %v", widths) } } } // WithConfig applies a custom configuration to the table by merging it with the default configuration. func WithConfig(cfg Config) Option { return func(target *Table) { target.config = mergeConfig(defaultConfig(), cfg) } } // WithDebug enables or disables debug logging and adjusts the logger level accordingly. // Logs the change if debugging is enabled. func WithDebug(debug bool) Option { return func(target *Table) { target.config.Debug = debug } } // WithFooter sets the table footers by calling the Footer method. func WithFooter(footers []string) Option { return func(target *Table) { target.Footer(footers) } } // WithFooterConfig applies a full footer configuration to the table. // Logs the change if debugging is enabled. func WithFooterConfig(config tw.CellConfig) Option { return func(target *Table) { target.config.Footer = config if target.logger != nil { target.logger.Debug("Option: WithFooterConfig applied to Table.") } } } // WithFooterAlignmentConfig applies a footer alignment configuration to the table. // Logs the change if debugging is enabled. func WithFooterAlignmentConfig(alignment tw.CellAlignment) Option { return func(target *Table) { target.config.Footer.Alignment = alignment if target.logger != nil { target.logger.Debugf("Option: WithFooterAlignmentConfig applied to Table: %+v", alignment) } } } // WithFooterMergeMode sets the merge mode for footer cells. // Invalid merge modes are ignored, and the change is logged if debugging is enabled. func WithFooterMergeMode(mergeMode int) Option { return func(target *Table) { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return } target.config.Footer.Formatting.MergeMode = mergeMode if target.logger != nil { target.logger.Debugf("Option: WithFooterMergeMode applied to Table: %v", mergeMode) } } } // WithFooterAutoWrap sets the wrapping behavior for footer cells. // Invalid wrap modes are ignored, and the change is logged if debugging is enabled. func WithFooterAutoWrap(wrap int) Option { return func(target *Table) { if wrap < tw.WrapNone || wrap > tw.WrapBreak { return } target.config.Footer.Formatting.AutoWrap = wrap if target.logger != nil { target.logger.Debugf("Option: WithFooterAutoWrap applied to Table: %v", wrap) } } } // WithFooterFilter sets the filter configuration for footer cells. // Logs the change if debugging is enabled. func WithFooterFilter(filter tw.CellFilter) Option { return func(target *Table) { target.config.Footer.Filter = filter if target.logger != nil { target.logger.Debug("Option: WithFooterFilter applied to Table.") } } } // WithFooterCallbacks sets the callback configuration for footer cells. // Logs the change if debugging is enabled. func WithFooterCallbacks(callbacks tw.CellCallbacks) Option { return func(target *Table) { target.config.Footer.Callbacks = callbacks if target.logger != nil { target.logger.Debug("Option: WithFooterCallbacks applied to Table.") } } } // WithFooterPaddingPerColumn sets per-column padding for footer cells. // Logs the change if debugging is enabled. func WithFooterPaddingPerColumn(padding []tw.Padding) Option { return func(target *Table) { target.config.Footer.Padding.PerColumn = padding if target.logger != nil { target.logger.Debugf("Option: WithFooterPaddingPerColumn applied to Table: %+v", padding) } } } // WithFooterMaxWidth sets the maximum content width for footer cells. // Negative values are ignored, and the change is logged if debugging is enabled. func WithFooterMaxWidth(maxWidth int) Option { return func(target *Table) { if maxWidth < 0 { return } target.config.Footer.ColMaxWidths.Global = maxWidth if target.logger != nil { target.logger.Debugf("Option: WithFooterMaxWidth applied to Table: %v", maxWidth) } } } // WithHeader sets the table headers by calling the Header method. func WithHeader(headers []string) Option { return func(target *Table) { target.Header(headers) } } // WithHeaderAlignment sets the text alignment for header cells. // Invalid alignments are ignored, and the change is logged if debugging is enabled. func WithHeaderAlignment(align tw.Align) Option { return func(target *Table) { if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone { return } target.config.Header.Alignment.Global = align if target.logger != nil { target.logger.Debugf("Option: WithHeaderAlignment applied to Table: %v", align) } } } // WithHeaderAutoWrap sets the wrapping behavior for header cells. // Invalid wrap modes are ignored, and the change is logged if debugging is enabled. func WithHeaderAutoWrap(wrap int) Option { return func(target *Table) { if wrap < tw.WrapNone || wrap > tw.WrapBreak { return } target.config.Header.Formatting.AutoWrap = wrap if target.logger != nil { target.logger.Debugf("Option: WithHeaderAutoWrap applied to Table: %v", wrap) } } } // WithHeaderMergeMode sets the merge mode for header cells. // Invalid merge modes are ignored, and the change is logged if debugging is enabled. func WithHeaderMergeMode(mergeMode int) Option { return func(target *Table) { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return } target.config.Header.Formatting.MergeMode = mergeMode if target.logger != nil { target.logger.Debugf("Option: WithHeaderMergeMode applied to Table: %v", mergeMode) } } } // WithHeaderFilter sets the filter configuration for header cells. // Logs the change if debugging is enabled. func WithHeaderFilter(filter tw.CellFilter) Option { return func(target *Table) { target.config.Header.Filter = filter if target.logger != nil { target.logger.Debug("Option: WithHeaderFilter applied to Table.") } } } // WithHeaderCallbacks sets the callback configuration for header cells. // Logs the change if debugging is enabled. func WithHeaderCallbacks(callbacks tw.CellCallbacks) Option { return func(target *Table) { target.config.Header.Callbacks = callbacks if target.logger != nil { target.logger.Debug("Option: WithHeaderCallbacks applied to Table.") } } } // WithHeaderPaddingPerColumn sets per-column padding for header cells. // Logs the change if debugging is enabled. func WithHeaderPaddingPerColumn(padding []tw.Padding) Option { return func(target *Table) { target.config.Header.Padding.PerColumn = padding if target.logger != nil { target.logger.Debugf("Option: WithHeaderPaddingPerColumn applied to Table: %+v", padding) } } } // WithHeaderMaxWidth sets the maximum content width for header cells. // Negative values are ignored, and the change is logged if debugging is enabled. func WithHeaderMaxWidth(maxWidth int) Option { return func(target *Table) { if maxWidth < 0 { return } target.config.Header.ColMaxWidths.Global = maxWidth if target.logger != nil { target.logger.Debugf("Option: WithHeaderMaxWidth applied to Table: %v", maxWidth) } } } // WithRowAlignment sets the text alignment for row cells. // Invalid alignments are ignored, and the change is logged if debugging is enabled. func WithRowAlignment(align tw.Align) Option { return func(target *Table) { if err := align.Validate(); err != nil { return } target.config.Row.Alignment.Global = align if target.logger != nil { target.logger.Debugf("Option: WithRowAlignment applied to Table: %v", align) } } } // WithRowAutoWrap sets the wrapping behavior for row cells. // Invalid wrap modes are ignored, and the change is logged if debugging is enabled. func WithRowAutoWrap(wrap int) Option { return func(target *Table) { if wrap < tw.WrapNone || wrap > tw.WrapBreak { return } target.config.Row.Formatting.AutoWrap = wrap if target.logger != nil { target.logger.Debugf("Option: WithRowAutoWrap applied to Table: %v", wrap) } } } // WithRowMergeMode sets the merge mode for row cells. // Invalid merge modes are ignored, and the change is logged if debugging is enabled. func WithRowMergeMode(mergeMode int) Option { return func(target *Table) { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return } target.config.Row.Formatting.MergeMode = mergeMode if target.logger != nil { target.logger.Debugf("Option: WithRowMergeMode applied to Table: %v", mergeMode) } } } // WithRowFilter sets the filter configuration for row cells. // Logs the change if debugging is enabled. func WithRowFilter(filter tw.CellFilter) Option { return func(target *Table) { target.config.Row.Filter = filter if target.logger != nil { target.logger.Debug("Option: WithRowFilter applied to Table.") } } } // WithRowCallbacks sets the callback configuration for row cells. // Logs the change if debugging is enabled. func WithRowCallbacks(callbacks tw.CellCallbacks) Option { return func(target *Table) { target.config.Row.Callbacks = callbacks if target.logger != nil { target.logger.Debug("Option: WithRowCallbacks applied to Table.") } } } // WithRowPaddingPerColumn sets per-column padding for row cells. // Logs the change if debugging is enabled. func WithRowPaddingPerColumn(padding []tw.Padding) Option { return func(target *Table) { target.config.Row.Padding.PerColumn = padding if target.logger != nil { target.logger.Debugf("Option: WithRowPaddingPerColumn applied to Table: %+v", padding) } } } // WithHeaderAlignmentConfig applies a header alignment configuration to the table. // Logs the change if debugging is enabled. func WithHeaderAlignmentConfig(alignment tw.CellAlignment) Option { return func(target *Table) { target.config.Header.Alignment = alignment if target.logger != nil { target.logger.Debugf("Option: WithHeaderAlignmentConfig applied to Table: %+v", alignment) } } } // WithHeaderConfig applies a full header configuration to the table. // Logs the change if debugging is enabled. func WithHeaderConfig(config tw.CellConfig) Option { return func(target *Table) { target.config.Header = config if target.logger != nil { target.logger.Debug("Option: WithHeaderConfig applied to Table.") } } } // WithLogger sets a custom logger for the table and updates the renderer if present. // Logs the change if debugging is enabled. func WithLogger(logger *ll.Logger) Option { return func(target *Table) { target.logger = logger if target.logger != nil { target.logger.Debug("Option: WithLogger applied to Table.") if target.renderer != nil { target.renderer.Logger(target.logger) } } } } // WithRenderer sets a custom renderer for the table and attaches the logger if present. // Logs the change if debugging is enabled. func WithRenderer(f tw.Renderer) Option { return func(target *Table) { target.renderer = f if target.logger != nil { target.logger.Debugf("Option: WithRenderer applied to Table: %T", f) f.Logger(target.logger) } } } // WithRowConfig applies a full row configuration to the table. // Logs the change if debugging is enabled. func WithRowConfig(config tw.CellConfig) Option { return func(target *Table) { target.config.Row = config if target.logger != nil { target.logger.Debug("Option: WithRowConfig applied to Table.") } } } // WithRowAlignmentConfig applies a row alignment configuration to the table. // Logs the change if debugging is enabled. func WithRowAlignmentConfig(alignment tw.CellAlignment) Option { return func(target *Table) { target.config.Row.Alignment = alignment if target.logger != nil { target.logger.Debugf("Option: WithRowAlignmentConfig applied to Table: %+v", alignment) } } } // WithRowMaxWidth sets the maximum content width for row cells. // Negative values are ignored, and the change is logged if debugging is enabled. func WithRowMaxWidth(maxWidth int) Option { return func(target *Table) { if maxWidth < 0 { return } target.config.Row.ColMaxWidths.Global = maxWidth if target.logger != nil { target.logger.Debugf("Option: WithRowMaxWidth applied to Table: %v", maxWidth) } } } // WithStreaming applies a streaming configuration to the table by merging it with the existing configuration. // Logs the change if debugging is enabled. func WithStreaming(c tw.StreamConfig) Option { return func(target *Table) { target.config.Stream = mergeStreamConfig(target.config.Stream, c) if target.logger != nil { target.logger.Debug("Option: WithStreaming applied to Table.") } } } // WithStringer sets a custom stringer function for converting row data and clears the stringer cache. // Logs the change if debugging is enabled. func WithStringer(stringer interface{}) Option { return func(t *Table) { t.stringer = stringer t.stringerCacheMu.Lock() t.stringerCache = make(map[reflect.Type]reflect.Value) t.stringerCacheMu.Unlock() if t.logger != nil { t.logger.Debug("Stringer updated, cache cleared") } } } // WithStringerCache enables caching for the stringer function. // Logs the change if debugging is enabled. func WithStringerCache() Option { return func(t *Table) { t.stringerCacheEnabled = true if t.logger != nil { t.logger.Debug("Option: WithStringerCache enabled") } } } // WithTrimSpace sets whether leading and trailing spaces are automatically trimmed. // Logs the change if debugging is enabled. func WithTrimSpace(state tw.State) Option { return func(target *Table) { target.config.Behavior.TrimSpace = state if target.logger != nil { target.logger.Debugf("Option: WithTrimSpace applied to Table: %v", state) } } } // WithHeaderAutoFormat enables or disables automatic formatting for header cells. // Logs the change if debugging is enabled. func WithHeaderAutoFormat(state tw.State) Option { return func(target *Table) { target.config.Header.Formatting.AutoFormat = state if target.logger != nil { target.logger.Debugf("Option: WithHeaderAutoFormat applied to Table: %v", state) } } } // WithFooterAutoFormat enables or disables automatic formatting for footer cells. // Logs the change if debugging is enabled. func WithFooterAutoFormat(state tw.State) Option { return func(target *Table) { target.config.Footer.Formatting.AutoFormat = state if target.logger != nil { target.logger.Debugf("Option: WithFooterAutoFormat applied to Table: %v", state) } } } // WithRowAutoFormat enables or disables automatic formatting for row cells. // Logs the change if debugging is enabled. func WithRowAutoFormat(state tw.State) Option { return func(target *Table) { target.config.Row.Formatting.AutoFormat = state if target.logger != nil { target.logger.Debugf("Option: WithRowAutoFormat applied to Table: %v", state) } } } // WithHeaderControl sets the control behavior for the table header. // Logs the change if debugging is enabled. func WithHeaderControl(control tw.Control) Option { return func(target *Table) { target.config.Behavior.Header = control if target.logger != nil { target.logger.Debugf("Option: WithHeaderControl applied to Table: %v", control) } } } // WithFooterControl sets the control behavior for the table footer. // Logs the change if debugging is enabled. func WithFooterControl(control tw.Control) Option { return func(target *Table) { target.config.Behavior.Footer = control if target.logger != nil { target.logger.Debugf("Option: WithFooterControl applied to Table: %v", control) } } } // WithAlignment sets the default column alignment for the header, rows, and footer. // Logs the change if debugging is enabled. func WithAlignment(alignment tw.Alignment) Option { return func(target *Table) { target.config.Header.Alignment.PerColumn = alignment target.config.Row.Alignment.PerColumn = alignment target.config.Footer.Alignment.PerColumn = alignment if target.logger != nil { target.logger.Debugf("Option: WithAlignment applied to Table: %+v", alignment) } } } // WithBehavior applies a behavior configuration to the table. // Logs the change if debugging is enabled. func WithBehavior(behavior tw.Behavior) Option { return func(target *Table) { target.config.Behavior = behavior if target.logger != nil { target.logger.Debugf("Option: WithBehavior applied to Table: %+v", behavior) } } } // WithPadding sets the global padding for the header, rows, and footer. // Logs the change if debugging is enabled. func WithPadding(padding tw.Padding) Option { return func(target *Table) { target.config.Header.Padding.Global = padding target.config.Row.Padding.Global = padding target.config.Footer.Padding.Global = padding if target.logger != nil { target.logger.Debugf("Option: WithPadding applied to Table: %+v", padding) } } } // WithRendition allows updating the active renderer's rendition configuration // by merging the provided rendition. // If the renderer does not implement tw.Renditioning, a warning is logged. // Logs the change if debugging is enabled. func WithRendition(rendition tw.Rendition) Option { return func(target *Table) { if target.renderer == nil { if target.logger != nil { target.logger.Warn("Option: WithRendition: No renderer set on table.") } return } if ru, ok := target.renderer.(tw.Renditioning); ok { ru.Rendition(rendition) if target.logger != nil { target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", rendition) } } else { if target.logger != nil { target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer) } } } } // WithEastAsian configures the global East Asian width calculation setting. // - enable=true: Enables East Asian width calculations. CJK and ambiguous characters // are typically measured as double width. // - enable=false: Disables East Asian width calculations. Characters are generally // measured as single width, subject to Unicode standards. // // This setting affects all subsequent display width calculations using the twdw package. func WithEastAsian(enable bool) Option { return func(target *Table) { twwidth.SetEastAsian(enable) } } // WithCondition provides a way to set a custom global runewidth.Condition // that will be used for all subsequent display width calculations by the twwidth (twdw) package. // // The runewidth.Condition object allows for more fine-grained control over how rune widths // are determined, beyond just toggling EastAsianWidth. This could include settings for // ambiguous width characters or other future properties of runewidth.Condition. func WithCondition(condition *runewidth.Condition) Option { return func(target *Table) { twwidth.SetCondition(condition) } } // WithSymbols sets the symbols used for drawing table borders and separators. // The symbols are applied to the table's renderer configuration, if a renderer is set. // If no renderer is set (target.renderer is nil), this option has no effect. . func WithSymbols(symbols tw.Symbols) Option { return func(target *Table) { if target.renderer != nil { cfg := target.renderer.Config() cfg.Symbols = symbols if ru, ok := target.renderer.(tw.Renditioning); ok { ru.Rendition(cfg) if target.logger != nil { target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", cfg) } } else { if target.logger != nil { target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer) } } } } } // defaultConfig returns a default Config with sensible settings for headers, rows, footers, and behavior. func defaultConfig() Config { return Config{ MaxWidth: 0, Header: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapTruncate, AutoFormat: tw.On, MergeMode: tw.MergeNone, }, Padding: tw.CellPadding{ Global: tw.PaddingDefault, }, Alignment: tw.CellAlignment{ Global: tw.AlignCenter, PerColumn: []tw.Align{}, }, }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapNormal, AutoFormat: tw.Off, MergeMode: tw.MergeNone, }, Padding: tw.CellPadding{ Global: tw.PaddingDefault, }, Alignment: tw.CellAlignment{ Global: tw.AlignLeft, PerColumn: []tw.Align{}, }, }, Footer: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapNormal, AutoFormat: tw.Off, MergeMode: tw.MergeNone, }, Padding: tw.CellPadding{ Global: tw.PaddingDefault, }, Alignment: tw.CellAlignment{ Global: tw.AlignRight, PerColumn: []tw.Align{}, }, }, Stream: tw.StreamConfig{ Enable: false, StrictColumns: false, }, Debug: false, Behavior: tw.Behavior{ AutoHide: tw.Off, TrimSpace: tw.On, Structs: tw.Struct{ AutoHeader: tw.Off, Tags: []string{"json", "db"}, }, }, } } // mergeCellConfig merges a source CellConfig into a destination CellConfig, prioritizing non-default source values. // It handles deep merging for complex fields like padding and callbacks. func mergeCellConfig(dst, src tw.CellConfig) tw.CellConfig { if src.Formatting.Alignment != tw.Empty { dst.Formatting.Alignment = src.Formatting.Alignment } if src.Formatting.AutoWrap != 0 { dst.Formatting.AutoWrap = src.Formatting.AutoWrap } if src.ColMaxWidths.Global != 0 { dst.ColMaxWidths.Global = src.ColMaxWidths.Global } if src.Formatting.MergeMode != 0 { dst.Formatting.MergeMode = src.Formatting.MergeMode } dst.Formatting.AutoFormat = src.Formatting.AutoFormat if src.Padding.Global.Paddable() { dst.Padding.Global = src.Padding.Global } if len(src.Padding.PerColumn) > 0 { if dst.Padding.PerColumn == nil { dst.Padding.PerColumn = make([]tw.Padding, len(src.Padding.PerColumn)) } else if len(src.Padding.PerColumn) > len(dst.Padding.PerColumn) { dst.Padding.PerColumn = append(dst.Padding.PerColumn, make([]tw.Padding, len(src.Padding.PerColumn)-len(dst.Padding.PerColumn))...) } for i, pad := range src.Padding.PerColumn { if pad.Paddable() { dst.Padding.PerColumn[i] = pad } } } if src.Callbacks.Global != nil { dst.Callbacks.Global = src.Callbacks.Global } if len(src.Callbacks.PerColumn) > 0 { if dst.Callbacks.PerColumn == nil { dst.Callbacks.PerColumn = make([]func(), len(src.Callbacks.PerColumn)) } else if len(src.Callbacks.PerColumn) > len(dst.Callbacks.PerColumn) { dst.Callbacks.PerColumn = append(dst.Callbacks.PerColumn, make([]func(), len(src.Callbacks.PerColumn)-len(dst.Callbacks.PerColumn))...) } for i, cb := range src.Callbacks.PerColumn { if cb != nil { dst.Callbacks.PerColumn[i] = cb } } } if src.Filter.Global != nil { dst.Filter.Global = src.Filter.Global } if len(src.Filter.PerColumn) > 0 { if dst.Filter.PerColumn == nil { dst.Filter.PerColumn = make([]func(string) string, len(src.Filter.PerColumn)) } else if len(src.Filter.PerColumn) > len(dst.Filter.PerColumn) { dst.Filter.PerColumn = append(dst.Filter.PerColumn, make([]func(string) string, len(src.Filter.PerColumn)-len(dst.Filter.PerColumn))...) } for i, filter := range src.Filter.PerColumn { if filter != nil { dst.Filter.PerColumn[i] = filter } } } // Merge Alignment if src.Alignment.Global != tw.Empty { dst.Alignment.Global = src.Alignment.Global } if len(src.Alignment.PerColumn) > 0 { if dst.Alignment.PerColumn == nil { dst.Alignment.PerColumn = make([]tw.Align, len(src.Alignment.PerColumn)) } else if len(src.Alignment.PerColumn) > len(dst.Alignment.PerColumn) { dst.Alignment.PerColumn = append(dst.Alignment.PerColumn, make([]tw.Align, len(src.Alignment.PerColumn)-len(dst.Alignment.PerColumn))...) } for i, align := range src.Alignment.PerColumn { if align != tw.Skip { dst.Alignment.PerColumn[i] = align } } } if len(src.ColumnAligns) > 0 { if dst.ColumnAligns == nil { dst.ColumnAligns = make([]tw.Align, len(src.ColumnAligns)) } else if len(src.ColumnAligns) > len(dst.ColumnAligns) { dst.ColumnAligns = append(dst.ColumnAligns, make([]tw.Align, len(src.ColumnAligns)-len(dst.ColumnAligns))...) } for i, align := range src.ColumnAligns { if align != tw.Skip { dst.ColumnAligns[i] = align } } } if len(src.ColMaxWidths.PerColumn) > 0 { if dst.ColMaxWidths.PerColumn == nil { dst.ColMaxWidths.PerColumn = make(map[int]int) } for k, v := range src.ColMaxWidths.PerColumn { if v != 0 { dst.ColMaxWidths.PerColumn[k] = v } } } return dst } // mergeConfig merges a source Config into a destination Config, prioritizing non-default source values. // It performs deep merging for complex types like Header, Row, Footer, and Stream. func mergeConfig(dst, src Config) Config { if src.MaxWidth != 0 { dst.MaxWidth = src.MaxWidth } dst.Debug = src.Debug || dst.Debug dst.Behavior.AutoHide = src.Behavior.AutoHide dst.Behavior.TrimSpace = src.Behavior.TrimSpace dst.Behavior.Compact = src.Behavior.Compact dst.Behavior.Header = src.Behavior.Header dst.Behavior.Footer = src.Behavior.Footer dst.Behavior.Footer = src.Behavior.Footer dst.Behavior.Structs.AutoHeader = src.Behavior.Structs.AutoHeader // check lent of tags if len(src.Behavior.Structs.Tags) > 0 { dst.Behavior.Structs.Tags = src.Behavior.Structs.Tags } if src.Widths.Global != 0 { dst.Widths.Global = src.Widths.Global } if len(src.Widths.PerColumn) > 0 { if dst.Widths.PerColumn == nil { dst.Widths.PerColumn = make(map[int]int) } for k, v := range src.Widths.PerColumn { if v != 0 { dst.Widths.PerColumn[k] = v } } } dst.Header = mergeCellConfig(dst.Header, src.Header) dst.Row = mergeCellConfig(dst.Row, src.Row) dst.Footer = mergeCellConfig(dst.Footer, src.Footer) dst.Stream = mergeStreamConfig(dst.Stream, src.Stream) return dst } // mergeStreamConfig merges a source StreamConfig into a destination StreamConfig, prioritizing non-default source values. func mergeStreamConfig(dst, src tw.StreamConfig) tw.StreamConfig { if src.Enable { dst.Enable = true } dst.StrictColumns = src.StrictColumns return dst } // padLine pads a line to the specified column count by appending empty strings as needed. func padLine(line []string, numCols int) []string { if len(line) >= numCols { return line } padded := make([]string, numCols) copy(padded, line) for i := len(line); i < numCols; i++ { padded[i] = tw.Empty } return padded } tablewriter-1.0.9/pkg/000077500000000000000000000000001504170215200146235ustar00rootroot00000000000000tablewriter-1.0.9/pkg/twwarp/000077500000000000000000000000001504170215200161475ustar00rootroot00000000000000tablewriter-1.0.9/pkg/twwarp/_data/000077500000000000000000000000001504170215200172175ustar00rootroot00000000000000tablewriter-1.0.9/pkg/twwarp/_data/long-text-wrapped.txt000066400000000000000000000072071504170215200233470ustar00rootroot00000000000000Я к вам пишу — чего же боле? Что я могу еще сказать? Теперь, я знаю, в вашей воле Меня презреньем наказать. Но вы, к моей несчастной доле Хоть каплю жалости храня, Вы не оставите меня. Сначала я молчать хотела; Поверьте: моего стыда Вы не узнали б никогда, Когда б надежду я имела Хоть редко, хоть в неделю раз В деревне нашей видеть вас, Чтоб только слышать ваши речи, Вам слово молвить, и потом Все думать, думать об одном И день и ночь до новой встречи. Но, говорят, вы нелюдим; В глуши, в деревне всё вам скучно, А мы… ничем мы не блестим, Хоть вам и рады простодушно. Зачем вы посетили нас? В глуши забытого селенья Я никогда не знала б вас, Не знала б горького мученья. Души неопытной волненья Смирив со временем (как знать?), По сердцу я нашла бы друга, Была бы верная супруга И добродетельная мать. Другой!.. Нет, никому на свете Не отдала бы сердца я! То в вышнем суждено совете… То воля неба: я твоя; Вся жизнь моя была залогом Свиданья верного с тобой; Я знаю, ты мне послан богом, До гроба ты хранитель мой… Ты в сновиденьях мне являлся, Незримый, ты мне был уж мил, Твой чудный взгляд меня томил, В душе твой голос раздавался Давно… нет, это был не сон! Ты чуть вошел, я вмиг узнала, Вся обомлела, запылала И в мыслях молвила: вот он! Не правда ль? Я тебя слыхала: Ты говорил со мной в тиши, Когда я бедным помогала Или молитвой услаждала Тоску волнуемой души? И в это самое мгновенье Не ты ли, милое виденье, В прозрачной темноте мелькнул, Приникнул тихо к изголовью? Не ты ль, с отрадой и любовью, Слова надежды мне шепнул? Кто ты, мой ангел ли хранитель, Или коварный искуситель: Мои сомненья разреши. Быть может, это все пустое, Обман неопытной души! И суждено совсем иное… Но так и быть! Судьбу мою Отныне я тебе вручаю, Перед тобою слезы лью, Твоей защиты умоляю… Вообрази: я здесь одна, Никто меня не понимает, Рассудок мой изнемогает, И молча гибнуть я должна. Я жду тебя: единым взором Надежды сердца оживи Иль сон тяжелый перерви, Увы, заслуженным укором! Кончаю! Страшно перечесть… Стыдом и страхом замираю… Но мне порукой ваша честь, И смело ей себя вверяю…tablewriter-1.0.9/pkg/twwarp/_data/long-text.txt000066400000000000000000000234731504170215200217120ustar00rootroot00000000000000 Я к вам пишу — чего же боле? Что я могу еще сказать? Теперь, я знаю, в вашей воле Меня презреньем наказать. Но вы, к моей несчастной доле Хоть каплю жалости храня, Вы не оставите меня. Сначала я молчать хотела; Поверьте: моего стыда Вы не узнали б никогда, Когда б надежду я имела Хоть редко, хоть в неделю раз В деревне нашей видеть вас, Чтоб только слышать ваши речи, Вам слово молвить, и потом Все думать, думать об одном И день и ночь до новой встречи. Но, говорят, вы нелюдим; В глуши, в деревне всё вам скучно, А мы… ничем мы не блестим, Хоть вам и рады простодушно. Зачем вы посетили нас? В глуши забытого селенья Я никогда не знала б вас, Не знала б горького мученья. Души неопытной волненья Смирив со временем (как знать?), По сердцу я нашла бы друга, Была бы верная супруга И добродетельная мать. Другой!.. Нет, никому на свете Не отдала бы сердца я! То в вышнем суждено совете… То воля неба: я твоя; Вся жизнь моя была залогом Свиданья верного с тобой; Я знаю, ты мне послан богом, До гроба ты хранитель мой… Ты в сновиденьях мне являлся, Незримый, ты мне был уж мил, Твой чудный взгляд меня томил, В душе твой голос раздавался Давно… нет, это был не сон! Ты чуть вошел, я вмиг узнала, Вся обомлела, запылала И в мыслях молвила: вот он! Не правда ль? Я тебя слыхала: Ты говорил со мной в тиши, Когда я бедным помогала Или молитвой услаждала Тоску волнуемой души? И в это самое мгновенье Не ты ли, милое виденье, В прозрачной темноте мелькнул, Приникнул тихо к изголовью? Не ты ль, с отрадой и любовью, Слова надежды мне шепнул? Кто ты, мой ангел ли хранитель, Или коварный искуситель: Мои сомненья разреши. Быть может, это все пустое, Обман неопытной души! И суждено совсем иное… Но так и быть! Судьбу мою Отныне я тебе вручаю, Перед тобою слезы лью, Твоей защиты умоляю… Вообрази: я здесь одна, Никто меня не понимает, Рассудок мой изнемогает, И молча гибнуть я должна. Я жду тебя: единым взором Надежды сердца оживи Иль сон тяжелый перерви, Увы, заслуженным укором! Кончаю! Страшно перечесть… Стыдом и страхом замираю… Но мне порукой ваша честь, И смело ей себя вверяю… tablewriter-1.0.9/pkg/twwarp/wrap.go000066400000000000000000000150101504170215200174440ustar00rootroot00000000000000// Copyright 2014 Oleku Konko All rights reserved. // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. // This module is a Table Writer API for the Go Programming Language. // The protocols were written in pure Go and works on windows and unix systems package twwarp import ( "math" "strings" "unicode" "github.com/olekukonko/tablewriter/pkg/twwidth" // IMPORT YOUR NEW PACKAGE "github.com/rivo/uniseg" // "github.com/mattn/go-runewidth" // This can be removed if all direct uses are gone ) const ( nl = "\n" sp = " " ) const defaultPenalty = 1e5 func SplitWords(s string) []string { words := make([]string, 0, len(s)/5) var wordBegin int wordPending := false for i, c := range s { if unicode.IsSpace(c) { if wordPending { words = append(words, s[wordBegin:i]) wordPending = false } continue } if !wordPending { wordBegin = i wordPending = true } } if wordPending { words = append(words, s[wordBegin:]) } return words } // WrapString wraps s into a paragraph of lines of length lim, with minimal // raggedness. func WrapString(s string, lim int) ([]string, int) { if s == sp { return []string{sp}, lim } words := SplitWords(s) if len(words) == 0 { return []string{""}, lim } var lines []string max := 0 for _, v := range words { // max = runewidth.StringWidth(v) // OLD max = twwidth.Width(v) // NEW: Use twdw.Width if max > lim { lim = max } } for _, line := range WrapWords(words, 1, lim, defaultPenalty) { lines = append(lines, strings.Join(line, sp)) } return lines, lim } // WrapStringWithSpaces wraps a string into lines of a specified display width while preserving // leading and trailing spaces. It splits the input string into words, condenses internal multiple // spaces to a single space, and wraps the content to fit within the given width limit, measured // using Unicode-aware display width. The function is used in the logging library to format log // messages for consistent output. It returns the wrapped lines as a slice of strings and the // adjusted width limit, which may increase if a single word exceeds the input limit. Thread-safe // as it does not modify shared state. func WrapStringWithSpaces(s string, lim int) ([]string, int) { if len(s) == 0 { return []string{""}, lim } if strings.TrimSpace(s) == "" { // All spaces // if runewidth.StringWidth(s) <= lim { // OLD if twwidth.Width(s) <= lim { // NEW: Use twdw.Width // return []string{s}, runewidth.StringWidth(s) // OLD return []string{s}, twwidth.Width(s) // NEW: Use twdw.Width } // For very long all-space strings, "wrap" by truncating to the limit. if lim > 0 { substring, _ := stringToDisplayWidth(s, lim) return []string{substring}, lim } return []string{""}, lim } var leadingSpaces, trailingSpaces, coreContent string firstNonSpace := strings.IndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) }) leadingSpaces = s[:firstNonSpace] lastNonSpace := strings.LastIndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) }) trailingSpaces = s[lastNonSpace+1:] coreContent = s[firstNonSpace : lastNonSpace+1] if coreContent == "" { return []string{leadingSpaces + trailingSpaces}, lim } words := SplitWords(coreContent) if len(words) == 0 { return []string{leadingSpaces + trailingSpaces}, lim } var lines []string currentLim := lim maxCoreWordWidth := 0 for _, v := range words { // w := runewidth.StringWidth(v) // OLD w := twwidth.Width(v) // NEW: Use twdw.Width if w > maxCoreWordWidth { maxCoreWordWidth = w } } if maxCoreWordWidth > currentLim { currentLim = maxCoreWordWidth } wrappedWordLines := WrapWords(words, 1, currentLim, defaultPenalty) for i, lineWords := range wrappedWordLines { joinedLine := strings.Join(lineWords, sp) finalLine := leadingSpaces + joinedLine if i == len(wrappedWordLines)-1 { // Last line finalLine += trailingSpaces } lines = append(lines, finalLine) } return lines, currentLim } // stringToDisplayWidth returns a substring of s that has a display width // as close as possible to, but not exceeding, targetWidth. // It returns the substring and its actual display width. func stringToDisplayWidth(s string, targetWidth int) (substring string, actualWidth int) { if targetWidth <= 0 { return "", 0 } var currentWidth int var endIndex int // Tracks the byte index in the original string g := uniseg.NewGraphemes(s) for g.Next() { grapheme := g.Str() // graphemeWidth := runewidth.StringWidth(grapheme) // OLD graphemeWidth := twwidth.Width(grapheme) // NEW: Use twdw.Width if currentWidth+graphemeWidth > targetWidth { break } currentWidth += graphemeWidth _, e := g.Positions() endIndex = e } return s[:endIndex], currentWidth } // WrapWords is the low-level line-breaking algorithm, useful if you need more // control over the details of the text wrapping process. For most uses, // WrapString will be sufficient and more convenient. // // WrapWords splits a list of words into lines with minimal "raggedness", // treating each rune as one unit, accounting for spc units between adjacent // words on each line, and attempting to limit lines to lim units. Raggedness // is the total error over all lines, where error is the square of the // difference of the length of the line and lim. Too-long lines (which only // happen when a single word is longer than lim units) have pen penalty units // added to the error. func WrapWords(words []string, spc, lim, pen int) [][]string { n := len(words) if n == 0 { return nil } lengths := make([]int, n) for i := 0; i < n; i++ { // lengths[i] = runewidth.StringWidth(words[i]) // OLD lengths[i] = twwidth.Width(words[i]) // NEW: Use twdw.Width } nbrk := make([]int, n) cost := make([]int, n) for i := range cost { cost[i] = math.MaxInt32 } remainderLen := lengths[n-1] // Uses updated lengths for i := n - 1; i >= 0; i-- { if i < n-1 { remainderLen += spc + lengths[i] } if remainderLen <= lim { cost[i] = 0 nbrk[i] = n continue } phraseLen := lengths[i] for j := i + 1; j < n; j++ { if j > i+1 { phraseLen += spc + lengths[j-1] } d := lim - phraseLen c := d*d + cost[j] if phraseLen > lim { c += pen // too-long lines get a worse penalty } if c < cost[i] { cost[i] = c nbrk[i] = j } } } var lines [][]string i := 0 for i < n { lines = append(lines, words[i:nbrk[i]]) i = nbrk[i] } return lines } // getLines decomposes a multiline string into a slice of strings. func getLines(s string) []string { return strings.Split(s, nl) } tablewriter-1.0.9/pkg/twwarp/wrap_test.go000066400000000000000000000107411504170215200205110ustar00rootroot00000000000000// Copyright 2014 Oleku Konko All rights reserved. // Use of this source code is governed by a MIT // license that can be found in the LICENSE file. // This module is a Table Writer API for the Go Programming Language. // The protocols were written in pure Go and works on windows and unix systems package twwarp import ( "bytes" "fmt" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" "os" "reflect" "runtime" "strings" "testing" "github.com/mattn/go-runewidth" ) var ( text = "The quick brown fox jumps over the lazy dog." testDir = "./_data" ) // checkEqual compares two values and fails the test if they are not equal func checkEqual(t *testing.T, got, want interface{}, msgs ...interface{}) { t.Helper() if !reflect.DeepEqual(got, want) { var buf bytes.Buffer buf.WriteString(fmt.Sprintf("got:\n[%v]\nwant:\n[%v]\n", got, want)) for _, v := range msgs { buf.WriteString(fmt.Sprint(v)) } t.Errorf(buf.String()) } } func TestWrap(t *testing.T) { exp := []string{ "The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog."} got, _ := WrapString(text, 6) checkEqual(t, len(got), len(exp)) } func TestWrapOneLine(t *testing.T) { exp := "The quick brown fox jumps over the lazy dog." words, _ := WrapString(text, 500) checkEqual(t, strings.Join(words, string(tw.Space)), exp) } func TestUnicode(t *testing.T) { input := "Česká řeřicha" var wordsUnicode []string if runewidth.IsEastAsian() { wordsUnicode, _ = WrapString(input, 14) } else { wordsUnicode, _ = WrapString(input, 13) } // input contains 13 (or 14 for CJK) runes, so it fits on one line. checkEqual(t, len(wordsUnicode), 1) } func TestDisplayWidth(t *testing.T) { input := "Česká řeřicha" want := 13 if runewidth.IsEastAsian() { want = 14 } if n := twwidth.Width(input); n != want { t.Errorf("Wants: %d Got: %d", want, n) } input = "\033[43;30m" + input + "\033[00m" checkEqual(t, twwidth.Width(input), want) input = "\033]8;;idea://open/?file=/path/somefile.php&line=12\033\\some URL\033]8;;\033\\" checkEqual(t, twwidth.Width(input), 8) } // WrapString was extremely memory greedy, it performed insane number of // allocations for what it was doing. See BenchmarkWrapString for details. func TestWrapStringAllocation(t *testing.T) { originalTextBytes, err := os.ReadFile(testDir + "/long-text.txt") if err != nil { t.Fatal(err) } originalText := string(originalTextBytes) wantWrappedBytes, err := os.ReadFile(testDir + "/long-text-wrapped.txt") if err != nil { t.Fatal(err) } wantWrappedText := string(wantWrappedBytes) var ms runtime.MemStats runtime.ReadMemStats(&ms) heapAllocBefore := int64(ms.HeapAlloc / 1024 / 1024) // When gotLines, gotLim := WrapString(originalText, 80) // Then wantLim := 80 if gotLim != wantLim { t.Errorf("Invalid limit: want=%d, got=%d", wantLim, gotLim) } gotWrappedText := strings.Join(gotLines, "\n") if gotWrappedText != wantWrappedText { t.Errorf("Invalid lines: want=\n%s\n got=\n%s", wantWrappedText, gotWrappedText) } runtime.ReadMemStats(&ms) heapAllocAfter := int64(ms.HeapAlloc / 1024 / 1024) heapAllocDelta := heapAllocAfter - heapAllocBefore if heapAllocDelta > 1 { t.Fatalf("heap allocation should not be greater than 1Mb, got=%dMb", heapAllocDelta) } } // Before optimization: // BenchmarkWrapString-16 1 2490331031 ns/op 2535184104 B/op 50905550 allocs/op // After optimization: // BenchmarkWrapString-16 1652 658098 ns/op 230223 B/op 5176 allocs/op func BenchmarkWrapString(b *testing.B) { d, err := os.ReadFile(testDir + "/long-text.txt") if err != nil { b.Fatal(err) } for i := 0; i < b.N; i++ { WrapString(string(d), 128) } } func TestSplitWords(t *testing.T) { for _, tt := range []struct { in string out []string }{{ in: "", out: []string{}, }, { in: "a", out: []string{"a"}, }, { in: "a b", out: []string{"a", "b"}, }, { in: " a b ", out: []string{"a", "b"}, }, { in: "\r\na\t\t \r\t b\r\n ", out: []string{"a", "b"}, }} { t.Run(tt.in, func(t *testing.T) { got := SplitWords(tt.in) if !reflect.DeepEqual(tt.out, got) { t.Errorf("want=%s, got=%s", tt.out, got) } }) } } func TestWrapString(t *testing.T) { want := []string{"ああああああああああああああああああああああああ", "あああああああ"} got, _ := WrapString("ああああああああああああああああああああああああ あああああああ", 55) checkEqual(t, got, want) } tablewriter-1.0.9/pkg/twwidth/000077500000000000000000000000001504170215200163155ustar00rootroot00000000000000tablewriter-1.0.9/pkg/twwidth/width.go000066400000000000000000000250251504170215200177670ustar00rootroot00000000000000package twwidth import ( "bytes" "github.com/mattn/go-runewidth" "regexp" "strings" "sync" ) // condition holds the global runewidth configuration, including East Asian width settings. var condition *runewidth.Condition // mu protects access to condition and widthCache for thread safety. var mu sync.Mutex // ansi is a compiled regular expression for stripping ANSI escape codes from strings. var ansi = Filter() func init() { condition = runewidth.NewCondition() widthCache = make(map[cacheKey]int) } // cacheKey is used as a key for memoizing string width results in widthCache. type cacheKey struct { str string // Input string eastAsianWidth bool // East Asian width setting } // widthCache stores memoized results of Width calculations to improve performance. var widthCache map[cacheKey]int // Filter compiles and returns a regular expression for matching ANSI escape sequences, // including CSI (Control Sequence Introducer) and OSC (Operating System Command) sequences. // The returned regex can be used to strip ANSI codes from strings. func Filter() *regexp.Regexp { var regESC = "\x1b" // ASCII escape character var regBEL = "\x07" // ASCII bell character // ANSI string terminator: either ESC+\ or BEL var regST = "(" + regexp.QuoteMeta(regESC+"\\") + "|" + regexp.QuoteMeta(regBEL) + ")" // Control Sequence Introducer (CSI): ESC[ followed by parameters and a final byte var regCSI = regexp.QuoteMeta(regESC+"[") + "[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]" // Operating System Command (OSC): ESC] followed by arbitrary content until a terminator var regOSC = regexp.QuoteMeta(regESC+"]") + ".*?" + regST // Combine CSI and OSC patterns into a single regex return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")") } // SetEastAsian enables or disables East Asian width handling for width calculations. // When the setting changes, the width cache is cleared to ensure accuracy. // This function is thread-safe. // // Example: // // twdw.SetEastAsian(true) // Enable East Asian width handling func SetEastAsian(enable bool) { mu.Lock() defer mu.Unlock() if condition.EastAsianWidth != enable { condition.EastAsianWidth = enable widthCache = make(map[cacheKey]int) // Clear cache on setting change } } // SetCondition updates the global runewidth.Condition used for width calculations. // When the condition is changed, the width cache is cleared. // This function is thread-safe. // // Example: // // newCond := runewidth.NewCondition() // newCond.EastAsianWidth = true // twdw.SetCondition(newCond) func SetCondition(newCond *runewidth.Condition) { mu.Lock() defer mu.Unlock() condition = newCond widthCache = make(map[cacheKey]int) // Clear cache on setting change } // Width calculates the visual width of a string, excluding ANSI escape sequences, // using the go-runewidth package for accurate Unicode handling. It accounts for the // current East Asian width setting and caches results for performance. // This function is thread-safe. // // Example: // // width := twdw.Width("Hello\x1b[31mWorld") // Returns 10 func Width(str string) int { mu.Lock() key := cacheKey{str: str, eastAsianWidth: condition.EastAsianWidth} if w, found := widthCache[key]; found { mu.Unlock() return w } mu.Unlock() // Use a temporary condition to avoid holding the lock during calculation tempCond := runewidth.NewCondition() tempCond.EastAsianWidth = key.eastAsianWidth stripped := ansi.ReplaceAllLiteralString(str, "") calculatedWidth := tempCond.StringWidth(stripped) mu.Lock() widthCache[key] = calculatedWidth mu.Unlock() return calculatedWidth } // WidthNoCache calculates the visual width of a string without using or // updating the global cache. It uses the current global East Asian width setting. // This function is intended for internal use (e.g., benchmarking) and is thread-safe. // // Example: // // width := twdw.WidthNoCache("Hello\x1b[31mWorld") // Returns 10 func WidthNoCache(str string) int { mu.Lock() currentEA := condition.EastAsianWidth mu.Unlock() tempCond := runewidth.NewCondition() tempCond.EastAsianWidth = currentEA stripped := ansi.ReplaceAllLiteralString(str, "") return tempCond.StringWidth(stripped) } // Display calculates the visual width of a string, excluding ANSI escape sequences, // using the provided runewidth condition. Unlike Width, it does not use caching // and is intended for cases where a specific condition is required. // This function is thread-safe with respect to the provided condition. // // Example: // // cond := runewidth.NewCondition() // width := twdw.Display(cond, "Hello\x1b[31mWorld") // Returns 10 func Display(cond *runewidth.Condition, str string) int { return cond.StringWidth(ansi.ReplaceAllLiteralString(str, "")) } // Truncate shortens a string to fit within a specified visual width, optionally // appending a suffix (e.g., "..."). It preserves ANSI escape sequences and adds // a reset sequence (\x1b[0m) if needed to prevent formatting bleed. The function // respects the global East Asian width setting and is thread-safe. // // If maxWidth is negative, an empty string is returned. If maxWidth is zero and // a suffix is provided, the suffix is returned. If the string's visual width is // less than or equal to maxWidth, the string (and suffix, if provided and fits) // is returned unchanged. // // Example: // // s := twdw.Truncate("Hello\x1b[31mWorld", 5, "...") // Returns "Hello..." // s = twdw.Truncate("Hello", 10) // Returns "Hello" func Truncate(s string, maxWidth int, suffix ...string) string { if maxWidth < 0 { return "" } suffixStr := strings.Join(suffix, "") sDisplayWidth := Width(s) // Uses global cached Width suffixDisplayWidth := Width(suffixStr) // Uses global cached Width // Case 1: Original string is visually empty. if sDisplayWidth == 0 { // If suffix is provided and fits within maxWidth (or if maxWidth is generous) if len(suffixStr) > 0 && suffixDisplayWidth <= maxWidth { return suffixStr } // If s has ANSI codes (len(s)>0) but maxWidth is 0, can't display them. if maxWidth == 0 && len(s) > 0 { return "" } return s // Returns "" or original ANSI codes } // Case 2: maxWidth is 0, but string has content. Cannot display anything. if maxWidth == 0 { return "" } // Case 3: String fits completely or fits with suffix. // Here, maxWidth is the total budget for the line. if sDisplayWidth <= maxWidth { if len(suffixStr) == 0 { // No suffix. return s } // Suffix is provided. Check if s + suffix fits. if sDisplayWidth+suffixDisplayWidth <= maxWidth { return s + suffixStr } // s fits, but s + suffix is too long. Return s. return s } // Case 4: String needs truncation (sDisplayWidth > maxWidth). // maxWidth is the total budget for the final string (content + suffix). // Capture the global EastAsianWidth setting once for consistent use mu.Lock() currentGlobalEastAsianWidth := condition.EastAsianWidth mu.Unlock() // Special case for EastAsian true: if only suffix fits, return suffix. // This was derived from previous test behavior. if len(suffixStr) > 0 && currentGlobalEastAsianWidth { provisionalContentWidth := maxWidth - suffixDisplayWidth if provisionalContentWidth == 0 { // Exactly enough space for suffix only return suffixStr // <<<< MODIFIED: No ANSI reset here } } // Calculate the budget for the content part, reserving space for the suffix. targetContentForIteration := maxWidth if len(suffixStr) > 0 { targetContentForIteration -= suffixDisplayWidth } // If content budget is negative, means not even suffix fits (or no suffix and no space). // However, if only suffix fits, it should be handled. if targetContentForIteration < 0 { // Can we still fit just the suffix? if len(suffixStr) > 0 && suffixDisplayWidth <= maxWidth { if strings.Contains(s, "\x1b[") { return "\x1b[0m" + suffixStr } return suffixStr } return "" // Cannot fit anything. } // If targetContentForIteration is 0, loop won't run, result will be empty string, then suffix is added. var contentBuf bytes.Buffer var currentContentDisplayWidth int var ansiSeqBuf bytes.Buffer inAnsiSequence := false ansiWrittenToContent := false localRunewidthCond := runewidth.NewCondition() localRunewidthCond.EastAsianWidth = currentGlobalEastAsianWidth for _, r := range s { if r == '\x1b' { inAnsiSequence = true ansiSeqBuf.Reset() ansiSeqBuf.WriteRune(r) } else if inAnsiSequence { ansiSeqBuf.WriteRune(r) seqBytes := ansiSeqBuf.Bytes() seqLen := len(seqBytes) terminated := false if seqLen >= 2 { introducer := seqBytes[1] if introducer == '[' { if seqLen >= 3 && r >= 0x40 && r <= 0x7E { terminated = true } } else if introducer == ']' { if r == '\x07' { terminated = true } else if seqLen > 1 && seqBytes[seqLen-2] == '\x1b' && r == '\\' { // Check for ST: \x1b\ terminated = true } } } if terminated { inAnsiSequence = false contentBuf.Write(ansiSeqBuf.Bytes()) ansiWrittenToContent = true ansiSeqBuf.Reset() } } else { // Normal character runeDisplayWidth := localRunewidthCond.RuneWidth(r) if targetContentForIteration == 0 { // No budget for content at all break } if currentContentDisplayWidth+runeDisplayWidth > targetContentForIteration { break } contentBuf.WriteRune(r) currentContentDisplayWidth += runeDisplayWidth } } result := contentBuf.String() // Suffix is added if: // 1. A suffix string is provided. // 2. Truncation actually happened (sDisplayWidth > maxWidth originally) // OR if the content part is empty but a suffix is meant to be shown // (e.g. targetContentForIteration was 0). if len(suffixStr) > 0 { // Add suffix if we are in the truncation path (sDisplayWidth > maxWidth) // OR if targetContentForIteration was 0 (meaning only suffix should be shown) // but we must ensure we don't exceed original maxWidth. // The logic above for targetContentForIteration already ensures space. needsReset := false // Condition for reset: if styling was active in 's' and might affect suffix if (ansiWrittenToContent || (inAnsiSequence && strings.Contains(s, "\x1b["))) && (currentContentDisplayWidth > 0 || ansiWrittenToContent) { if !strings.HasSuffix(result, "\x1b[0m") { needsReset = true } } else if currentContentDisplayWidth > 0 && strings.Contains(result, "\x1b[") && !strings.HasSuffix(result, "\x1b[0m") && strings.Contains(s, "\x1b[") { // If result has content and ANSI, and original had ANSI, and result not already reset needsReset = true } if needsReset { result += "\x1b[0m" } result += suffixStr } return result } tablewriter-1.0.9/pkg/twwidth/width_test.go000066400000000000000000000172021504170215200210240ustar00rootroot00000000000000package twwidth import ( "fmt" "os" "strconv" "strings" "sync" "testing" "github.com/mattn/go-runewidth" ) func TestMain(m *testing.M) { mu.Lock() condition = runewidth.NewCondition() mu.Unlock() os.Exit(m.Run()) } func TestFilter(t *testing.T) { ansi := Filter() tests := []struct { input string expected bool }{ {"\033[31m", true}, {"\033]8;;http://example.com\007", true}, {"hello", false}, {"\033[m", true}, {"\033[1;34;40m", true}, {"\033invalid", false}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { if got := ansi.MatchString(tt.input); got != tt.expected { t.Errorf("Filter().MatchString(%q) = %v, want %v", tt.input, got, tt.expected) } }) } } func TestSetEastAsian(t *testing.T) { original := condition.EastAsianWidth SetEastAsian(true) if !condition.EastAsianWidth { t.Errorf("SetEastAsian(true): condition.EastAsianWidth = false, want true") } SetEastAsian(false) if condition.EastAsianWidth { t.Errorf("SetEastAsian(false): condition.EastAsianWidth = true, want false") } mu.Lock() condition.EastAsianWidth = original mu.Unlock() } func TestWidth(t *testing.T) { tests := []struct { name string input string eastAsian bool expectedWidth int }{ { name: "ASCII", input: "hello", eastAsian: false, expectedWidth: 5, }, { name: "Unicode with ANSI", input: "\033[31m☆あ\033[0m", eastAsian: false, expectedWidth: 3, }, { name: "Unicode with EastAsian", input: "\033[31m☆あ\033[0m", eastAsian: true, expectedWidth: 4, }, { name: "Empty string", input: "", eastAsian: false, expectedWidth: 0, }, { name: "Only ANSI", input: "\033[31m\033[0m", eastAsian: false, expectedWidth: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { SetEastAsian(tt.eastAsian) got := Width(tt.input) if got != tt.expectedWidth { t.Errorf("Width(%q) = %d, want %d (EastAsian=%v)", tt.input, got, tt.expectedWidth, tt.eastAsian) } }) } } func TestDisplay(t *testing.T) { tests := []struct { name string input string eastAsian bool expectedWidth int }{ { name: "ASCII", input: "hello", eastAsian: false, expectedWidth: 5, }, { name: "Unicode with ANSI", input: "\033[31m☆あ\033[0m", eastAsian: false, expectedWidth: 3, }, { name: "Unicode with EastAsian", input: "\033[31m☆あ\033[0m", eastAsian: true, expectedWidth: 4, }, { name: "Empty string", input: "", eastAsian: false, expectedWidth: 0, }, { name: "Only ANSI", input: "\033[31m\033[0m", eastAsian: false, expectedWidth: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cond := &runewidth.Condition{EastAsianWidth: tt.eastAsian} got := Display(cond, tt.input) if got != tt.expectedWidth { t.Errorf("Display(%q, cond) = %d, want %d (EastAsian=%v)", tt.input, got, tt.expectedWidth, tt.eastAsian) } }) } } func TestTruncate(t *testing.T) { tests := []struct { name string input string maxWidth int suffix []string eastAsian bool expected string }{ { name: "ASCII within width", input: "hello", maxWidth: 5, suffix: nil, eastAsian: false, expected: "hello", }, { name: "ASCII with suffix", input: "hello", maxWidth: 8, suffix: []string{"..."}, eastAsian: false, expected: "hello...", }, { name: "ASCII truncate", input: "hello", maxWidth: 3, suffix: []string{"..."}, eastAsian: false, expected: "...", }, { name: "Unicode with ANSI, no truncate", input: "\033[31m☆あ\033[0m", maxWidth: 3, suffix: nil, eastAsian: false, expected: "\033[31m☆あ\033[0m", }, { name: "Unicode with ANSI, truncate", input: "\033[31m☆あ\033[0m", maxWidth: 2, suffix: []string{"..."}, eastAsian: false, expected: "", }, { name: "Unicode with EastAsian", input: "\033[31m☆あ\033[0m", maxWidth: 3, suffix: []string{"..."}, eastAsian: true, expected: "...", }, { name: "Zero maxWidth", input: "hello", maxWidth: 0, suffix: []string{"..."}, eastAsian: false, expected: "", }, { name: "Negative maxWidth", input: "hello", maxWidth: -1, suffix: []string{"..."}, eastAsian: false, expected: "", }, { name: "Empty string with suffix", input: "", maxWidth: 3, suffix: []string{"..."}, eastAsian: false, expected: "...", }, { name: "Only ANSI", input: "\033[31m\033[0m", maxWidth: 3, suffix: []string{"..."}, eastAsian: false, expected: "...", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { SetEastAsian(tt.eastAsian) got := Truncate(tt.input, tt.maxWidth, tt.suffix...) if got != tt.expected { t.Errorf("Truncate(%q, %d, %v) (EA=%v) = %q, want %q", tt.input, tt.maxWidth, tt.suffix, tt.eastAsian, got, tt.expected) } }) } } func TestConcurrentSetEastAsian(t *testing.T) { var wg sync.WaitGroup numGoroutines := 10 iterations := 100 for i := 0; i < numGoroutines; i++ { wg.Add(1) go func(enable bool) { defer wg.Done() for j := 0; j < iterations; j++ { SetEastAsian(enable) } }(i%2 == 0) } wg.Wait() } func TestWidthWithEnvironment(t *testing.T) { original := os.Getenv("RUNEWIDTH_EASTASIAN") defer func() { if original == "" { os.Unsetenv("RUNEWIDTH_EASTASIAN") } else { os.Setenv("RUNEWIDTH_EASTASIAN", original) } }() os.Setenv("RUNEWIDTH_EASTASIAN", "0") SetEastAsian(false) if got := Width("☆あ"); got != 3 { t.Errorf("Width(☆あ) with RUNEWIDTH_EASTASIAN=0 = %d, want 3", got) } os.Setenv("RUNEWIDTH_EASTASIAN", "1") SetEastAsian(true) if got := Width("☆あ"); got != 4 { t.Errorf("Width(☆あ) with RUNEWIDTH_EASTASIAN=1 = %d, want 4", got) } } func resetGlobalCache() { mu.Lock() widthCache = make(map[cacheKey]int) mu.Unlock() } var benchmarkStrings = map[string]string{ "SimpleASCII": "hello world, this is a test string.", "ASCIIWithANSI": "\033[31mhello\033[0m \033[34mworld\033[0m, this is \033[1ma\033[0m test string.", "EastAsian": "こんにちは世界、これはテスト文字列です。", "EastAsianWithANSI": "\033[32mこんにちは\033[0m \033[35m世界\033[0m、これは\033[4mテスト\033[0m文字列です。", "LongSimpleASCII": strings.Repeat("abcdefghijklmnopqrstuvwxyz ", 20), "LongASCIIWithANSI": strings.Repeat("\033[31ma\033[32mb\033[33mc\033[34md\033[35me\033[36mf\033[0m ", 50), } func BenchmarkWidthFunction(b *testing.B) { eastAsianSettings := []bool{false, true} for name, str := range benchmarkStrings { for _, eaSetting := range eastAsianSettings { SetEastAsian(eaSetting) b.Run(fmt.Sprintf("%s_EA%v_NoCache", name, eaSetting), func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _ = WidthNoCache(str) } }) b.Run(fmt.Sprintf("%s_EA%v_CacheMiss", name, eaSetting), func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _ = Width(str + strconv.Itoa(i)) } }) resetGlobalCache() b.Run(fmt.Sprintf("%s_EA%v_CacheHit", name, eaSetting), func(b *testing.B) { b.ReportAllocs() if b.N > 0 { _ = Width(str) } for i := 1; i < b.N; i++ { _ = Width(str) } }) resetGlobalCache() } } } tablewriter-1.0.9/renderer/000077500000000000000000000000001504170215200156505ustar00rootroot00000000000000tablewriter-1.0.9/renderer/blueprint.go000066400000000000000000000522601504170215200202100ustar00rootroot00000000000000package renderer import ( "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/pkg/twwidth" "io" "strings" "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 := width - padLeftWidth - padRightWidth if availableContentWidth < 0 { availableContentWidth = 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 := width - runeWidth if totalPaddingWidth < 0 { totalPaddingWidth = 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 := ctx.NormalizedWidths.Get(colIndex + k) if normWidth < 0 { normWidth = 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 if align == tw.AlignNone { if ctx.Row.Position == tw.Header { align = tw.AlignCenter } else if ctx.Row.Position == tw.Footer { align = tw.AlignRight } else { 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) } else if align == tw.Skip { if ctx.Row.Position == tw.Header { align = tw.AlignCenter } else if ctx.Row.Position == tw.Footer { align = tw.AlignRight } else { 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) tablewriter-1.0.9/renderer/colorized.go000066400000000000000000000602131504170215200201730ustar00rootroot00000000000000package renderer import ( "github.com/fatih/color" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/tablewriter/pkg/twwidth" "io" "strings" "github.com/olekukonko/tablewriter/tw" ) // ColorizedConfig holds configuration for the Colorized table renderer. type ColorizedConfig struct { Borders tw.Border // Border visibility settings Settings tw.Settings // Rendering behavior settings (e.g., separators, whitespace) Header Tint // Colors for header cells Column Tint // Colors for row cells Footer Tint // Colors for footer cells Border Tint // Colors for borders and lines Separator Tint // Colors for column separators Symbols tw.Symbols // Symbols for table drawing (e.g., corners, lines) } // Colors is a slice of color attributes for use with fatih/color, such as color.FgWhite or color.Bold. type Colors []color.Attribute // Tint defines foreground and background color settings for table elements, with optional per-column overrides. type Tint struct { FG Colors // Foreground color attributes BG Colors // Background color attributes Columns []Tint // Per-column color settings } // Apply applies the Tint's foreground and background colors to the given text, returning the text unchanged if no colors are set. func (t Tint) Apply(text string) string { if len(t.FG) == 0 && len(t.BG) == 0 { return text } // Combine foreground and background colors combinedColors := append(t.FG, t.BG...) // Create a color function and apply it to the text c := color.New(combinedColors...).SprintFunc() return c(text) } // Colorized renders colored ASCII tables with customizable borders, colors, and alignments. type Colorized struct { config ColorizedConfig // Renderer configuration trace []string // Debug trace messages newLine string // Newline character defaultAlign map[tw.Position]tw.Align // Default alignments for header, row, and footer logger *ll.Logger // Logger for debug messages w io.Writer } // NewColorized creates a Colorized renderer with the specified configuration, falling back to defaults if none provided. // Only the first config is used if multiple are passed. func NewColorized(configs ...ColorizedConfig) *Colorized { // Initialize with default configuration baseCfg := defaultColorized() if len(configs) > 0 { userCfg := configs[0] // Override border settings if provided if userCfg.Borders.Left != 0 { baseCfg.Borders.Left = userCfg.Borders.Left } if userCfg.Borders.Right != 0 { baseCfg.Borders.Right = userCfg.Borders.Right } if userCfg.Borders.Top != 0 { baseCfg.Borders.Top = userCfg.Borders.Top } if userCfg.Borders.Bottom != 0 { baseCfg.Borders.Bottom = userCfg.Borders.Bottom } // Merge separator and line settings baseCfg.Settings.Separators = mergeSeparators(baseCfg.Settings.Separators, userCfg.Settings.Separators) baseCfg.Settings.Lines = mergeLines(baseCfg.Settings.Lines, userCfg.Settings.Lines) // Override compact mode if specified if userCfg.Settings.CompactMode != 0 { baseCfg.Settings.CompactMode = userCfg.Settings.CompactMode } // Override color settings for various table elements if len(userCfg.Header.FG) > 0 || len(userCfg.Header.BG) > 0 || userCfg.Header.Columns != nil { baseCfg.Header = userCfg.Header } if len(userCfg.Column.FG) > 0 || len(userCfg.Column.BG) > 0 || userCfg.Column.Columns != nil { baseCfg.Column = userCfg.Column } if len(userCfg.Footer.FG) > 0 || len(userCfg.Footer.BG) > 0 || userCfg.Footer.Columns != nil { baseCfg.Footer = userCfg.Footer } if len(userCfg.Border.FG) > 0 || len(userCfg.Border.BG) > 0 || userCfg.Border.Columns != nil { baseCfg.Border = userCfg.Border } if len(userCfg.Separator.FG) > 0 || len(userCfg.Separator.BG) > 0 || userCfg.Separator.Columns != nil { baseCfg.Separator = userCfg.Separator } // Override symbols if provided if userCfg.Symbols != nil { baseCfg.Symbols = userCfg.Symbols } } cfg := baseCfg // Ensure symbols are initialized if cfg.Symbols == nil { cfg.Symbols = tw.NewSymbols(tw.StyleLight) } // Initialize the Colorized renderer f := &Colorized{ config: cfg, newLine: tw.NewLine, defaultAlign: map[tw.Position]tw.Align{ tw.Header: tw.AlignCenter, tw.Row: tw.AlignLeft, tw.Footer: tw.AlignRight, }, logger: ll.New("colorized", ll.WithHandler(lh.NewMemoryHandler())), } // Log initialization details f.logger.Debugf("Initialized Colorized renderer with symbols: Center=%q, Row=%q, Column=%q", f.config.Symbols.Center(), f.config.Symbols.Row(), f.config.Symbols.Column()) f.logger.Debugf("Final ColorizedConfig.Settings.Lines: %+v", f.config.Settings.Lines) f.logger.Debugf("Final ColorizedConfig.Borders: %+v", f.config.Borders) return f } // Close performs cleanup (no-op in this implementation). func (c *Colorized) Close() error { c.logger.Debug("Colorized.Close() called (no-op).") return nil } // Config returns the renderer's configuration as a Rendition. func (c *Colorized) Config() tw.Rendition { return tw.Rendition{ Borders: c.config.Borders, Settings: c.config.Settings, Symbols: c.config.Symbols, Streaming: true, } } // Debug returns the accumulated debug trace messages. func (c *Colorized) Debug() []string { return c.trace } // Footer renders the table footer with configured colors and formatting. func (c *Colorized) Footer(footers [][]string, ctx tw.Formatting) { c.logger.Debugf("Starting Footer render: IsSubRow=%v, Location=%v, Pos=%s", ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position) // Check if there are footers to render if len(footers) == 0 || len(footers[0]) == 0 { c.logger.Debug("Footer: No footers to render") return } // Render the footer line c.renderLine(ctx, footers[0], c.config.Footer) c.logger.Debug("Completed Footer render") } // Header renders the table header with configured colors and formatting. func (c *Colorized) Header(headers [][]string, ctx tw.Formatting) { c.logger.Debugf("Starting Header render: IsSubRow=%v, Location=%v, Pos=%s, lines=%d, widths=%v", ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, len(headers), ctx.Row.Widths) // Check if there are headers to render if len(headers) == 0 || len(headers[0]) == 0 { c.logger.Debug("Header: No headers to render") return } // Render the header line c.renderLine(ctx, headers[0], c.config.Header) c.logger.Debug("Completed Header render") } // Line renders a horizontal row line with colored junctions and segments, skipping zero-width columns. func (c *Colorized) Line(ctx tw.Formatting) { c.logger.Debugf("Line: Starting with Level=%v, Location=%v, IsSubRow=%v, Widths=%v", ctx.Level, ctx.Row.Location, ctx.IsSubRow, ctx.Row.Widths) // Initialize junction renderer jr := NewJunction(JunctionContext{ Symbols: c.config.Symbols, Ctx: ctx, ColIdx: 0, BorderTint: c.config.Border, SeparatorTint: c.config.Separator, Logger: c.logger, }) var line strings.Builder // Get sorted column indices and filter out zero-width columns allSortedKeys := ctx.Row.Widths.SortedKeys() effectiveKeys := []int{} keyWidthMap := make(map[int]int) for _, k := range allSortedKeys { width := ctx.Row.Widths.Get(k) keyWidthMap[k] = width if width > 0 { effectiveKeys = append(effectiveKeys, k) } } c.logger.Debugf("Line: All keys=%v, Effective keys (width>0)=%v", allSortedKeys, effectiveKeys) // Handle case with no effective columns if len(effectiveKeys) == 0 { prefix := tw.Empty suffix := tw.Empty if c.config.Borders.Left.Enabled() { prefix = jr.RenderLeft() } if c.config.Borders.Right.Enabled() { originalLastColIdx := -1 if len(allSortedKeys) > 0 { originalLastColIdx = allSortedKeys[len(allSortedKeys)-1] } suffix = jr.RenderRight(originalLastColIdx) } if prefix != tw.Empty || suffix != tw.Empty { line.WriteString(prefix + suffix + tw.NewLine) c.w.Write([]byte(line.String())) } c.logger.Debug("Line: Handled empty row/widths case (no effective keys)") return } // Add left border if enabled if c.config.Borders.Left.Enabled() { line.WriteString(jr.RenderLeft()) } // Render segments for each effective column for keyIndex, currentColIdx := range effectiveKeys { jr.colIdx = currentColIdx segment := jr.GetSegment() colWidth := keyWidthMap[currentColIdx] c.logger.Debugf("Line: Drawing segment for Effective colIdx=%d, segment='%s', width=%d", currentColIdx, segment, colWidth) if segment == tw.Empty { line.WriteString(strings.Repeat(tw.Space, colWidth)) } else { // Calculate how many times to repeat the segment segmentWidth := twwidth.Width(segment) if segmentWidth <= 0 { segmentWidth = 1 } repeat := 0 if colWidth > 0 && segmentWidth > 0 { repeat = colWidth / segmentWidth } drawnSegment := strings.Repeat(segment, repeat) line.WriteString(drawnSegment) // Adjust for width discrepancies actualDrawnWidth := twwidth.Width(drawnSegment) if actualDrawnWidth < colWidth { missingWidth := colWidth - actualDrawnWidth spaces := strings.Repeat(tw.Space, missingWidth) if len(c.config.Border.BG) > 0 { line.WriteString(Tint{BG: c.config.Border.BG}.Apply(spaces)) } else { line.WriteString(spaces) } c.logger.Debugf("Line: colIdx=%d corrected segment width, added %d spaces", currentColIdx, missingWidth) } else if actualDrawnWidth > colWidth { c.logger.Debugf("Line: WARNING colIdx=%d segment draw width %d > target %d", currentColIdx, actualDrawnWidth, colWidth) } } // Add junction between columns if not the last visible column isLastVisible := keyIndex == len(effectiveKeys)-1 if !isLastVisible && c.config.Settings.Separators.BetweenColumns.Enabled() { nextVisibleColIdx := effectiveKeys[keyIndex+1] originalPrecedingCol := -1 foundCurrent := false for _, k := range allSortedKeys { if k == currentColIdx { foundCurrent = true } if foundCurrent && k < nextVisibleColIdx { originalPrecedingCol = k } if k >= nextVisibleColIdx { break } } if originalPrecedingCol != -1 { jr.colIdx = originalPrecedingCol junction := jr.RenderJunction(originalPrecedingCol, nextVisibleColIdx) c.logger.Debugf("Line: Junction between visible %d (orig preceding %d) and next visible %d: '%s'", currentColIdx, originalPrecedingCol, nextVisibleColIdx, junction) line.WriteString(junction) } else { c.logger.Debugf("Line: Could not determine original preceding column for junction before visible %d", nextVisibleColIdx) line.WriteString(c.config.Separator.Apply(jr.sym.Center())) } } } // Add right border if enabled if c.config.Borders.Right.Enabled() { originalLastColIdx := -1 if len(allSortedKeys) > 0 { originalLastColIdx = allSortedKeys[len(allSortedKeys)-1] } jr.colIdx = originalLastColIdx line.WriteString(jr.RenderRight(originalLastColIdx)) } // Write the final line line.WriteString(c.newLine) c.w.Write([]byte(line.String())) c.logger.Debugf("Line rendered: %s", strings.TrimSuffix(line.String(), c.newLine)) } // Logger sets the logger for the Colorized instance. func (c *Colorized) Logger(logger *ll.Logger) { c.logger = logger.Namespace("colorized") } // Reset clears the renderer's internal state, including debug traces. func (c *Colorized) Reset() { c.trace = nil c.logger.Debugf("Reset: Cleared debug trace") } // Row renders a table data row with configured colors and formatting. func (c *Colorized) Row(row []string, ctx tw.Formatting) { c.logger.Debugf("Starting Row render: IsSubRow=%v, Location=%v, Pos=%s, hasFooter=%v", ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, ctx.HasFooter) // Check if there is data to render if len(row) == 0 { c.logger.Debugf("Row: No data to render") return } // Render the row line c.renderLine(ctx, row, c.config.Column) c.logger.Debugf("Completed Row render") } // Start initializes the rendering process (no-op in this implementation). func (c *Colorized) Start(w io.Writer) error { c.w = w c.logger.Debugf("Colorized.Start() called (no-op).") return nil } // formatCell formats a cell's content with color, width, padding, and alignment, handling whitespace trimming and truncation. func (c *Colorized) formatCell(content string, width int, padding tw.Padding, align tw.Align, tint Tint) string { c.logger.Debugf("Formatting cell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s', tintFG=%v, tintBG=%v", content, width, align, padding.Left, padding.Right, tint.FG, tint.BG) // Return empty string if width is non-positive if width <= 0 { c.logger.Debugf("formatCell: width %d <= 0, returning empty string", width) return tw.Empty } // Calculate visual width of content contentVisualWidth := twwidth.Width(content) // Set default padding characters padLeftCharStr := padding.Left if padLeftCharStr == tw.Empty { padLeftCharStr = tw.Space } padRightCharStr := padding.Right if padRightCharStr == tw.Empty { padRightCharStr = tw.Space } // Calculate padding widths definedPadLeftWidth := twwidth.Width(padLeftCharStr) definedPadRightWidth := twwidth.Width(padRightCharStr) // Calculate available width for content and alignment availableForContentAndAlign := width - definedPadLeftWidth - definedPadRightWidth if availableForContentAndAlign < 0 { availableForContentAndAlign = 0 } // Truncate content if it exceeds available width if contentVisualWidth > availableForContentAndAlign { content = twwidth.Truncate(content, availableForContentAndAlign) contentVisualWidth = twwidth.Width(content) c.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableForContentAndAlign, content, contentVisualWidth) } // Calculate remaining space for alignment remainingSpaceForAlignment := availableForContentAndAlign - contentVisualWidth if remainingSpaceForAlignment < 0 { remainingSpaceForAlignment = 0 } // Apply alignment padding leftAlignmentPadSpaces := tw.Empty rightAlignmentPadSpaces := tw.Empty switch align { case tw.AlignLeft: rightAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment) case tw.AlignRight: leftAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment) case tw.AlignCenter: leftSpacesCount := remainingSpaceForAlignment / 2 rightSpacesCount := remainingSpaceForAlignment - leftSpacesCount leftAlignmentPadSpaces = strings.Repeat(tw.Space, leftSpacesCount) rightAlignmentPadSpaces = strings.Repeat(tw.Space, rightSpacesCount) default: // Default to left alignment rightAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment) } // Apply colors to content and padding coloredContent := tint.Apply(content) coloredPadLeft := padLeftCharStr coloredPadRight := padRightCharStr coloredAlignPadLeft := leftAlignmentPadSpaces coloredAlignPadRight := rightAlignmentPadSpaces if len(tint.BG) > 0 { bgTint := Tint{BG: tint.BG} // Apply foreground color to non-space padding if foreground is defined if len(tint.FG) > 0 && padLeftCharStr != tw.Space { coloredPadLeft = tint.Apply(padLeftCharStr) } else { coloredPadLeft = bgTint.Apply(padLeftCharStr) } if len(tint.FG) > 0 && padRightCharStr != tw.Space { coloredPadRight = tint.Apply(padRightCharStr) } else { coloredPadRight = bgTint.Apply(padRightCharStr) } // Apply background color to alignment padding if leftAlignmentPadSpaces != tw.Empty { coloredAlignPadLeft = bgTint.Apply(leftAlignmentPadSpaces) } if rightAlignmentPadSpaces != tw.Empty { coloredAlignPadRight = bgTint.Apply(rightAlignmentPadSpaces) } } else if len(tint.FG) > 0 { // Apply foreground color to non-space padding if padLeftCharStr != tw.Space { coloredPadLeft = tint.Apply(padLeftCharStr) } if padRightCharStr != tw.Space { coloredPadRight = tint.Apply(padRightCharStr) } } // Build final cell string var sb strings.Builder sb.WriteString(coloredPadLeft) sb.WriteString(coloredAlignPadLeft) sb.WriteString(coloredContent) sb.WriteString(coloredAlignPadRight) sb.WriteString(coloredPadRight) output := sb.String() // Adjust output width if necessary currentVisualWidth := twwidth.Width(output) if currentVisualWidth != width { c.logger.Debugf("formatCell MISMATCH: content='%s', target_w=%d. Calculated parts width = %d. String: '%s'", content, width, currentVisualWidth, output) if currentVisualWidth > width { output = twwidth.Truncate(output, width) } else { paddingSpacesStr := strings.Repeat(tw.Space, width-currentVisualWidth) if len(tint.BG) > 0 { output += Tint{BG: tint.BG}.Apply(paddingSpacesStr) } else { output += paddingSpacesStr } } c.logger.Debugf("formatCell Post-Correction: Target %d, New Visual width %d. Output: '%s'", width, twwidth.Width(output), output) } c.logger.Debugf("Formatted cell final result: '%s' (target width %d, display width %d)", output, width, twwidth.Width(output)) return output } // renderLine renders a single line (header, row, or footer) with colors, handling merges and separators. func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) { // Determine number of columns numCols := 0 if len(ctx.Row.Current) > 0 { maxKey := -1 for k := range ctx.Row.Current { if k > maxKey { maxKey = k } } numCols = maxKey + 1 } else { maxKey := -1 for k := range ctx.Row.Widths { if k > maxKey { maxKey = k } } numCols = maxKey + 1 } var output strings.Builder // Add left border if enabled prefix := tw.Empty if c.config.Borders.Left.Enabled() { prefix = c.config.Border.Apply(c.config.Symbols.Column()) } output.WriteString(prefix) // Set up separator separatorDisplayWidth := 0 separatorString := tw.Empty if c.config.Settings.Separators.BetweenColumns.Enabled() { separatorString = c.config.Separator.Apply(c.config.Symbols.Column()) separatorDisplayWidth = twwidth.Width(c.config.Symbols.Column()) } // Process each column for i := 0; i < numCols; { // Determine if a separator is needed shouldAddSeparator := false if i > 0 && c.config.Settings.Separators.BetweenColumns.Enabled() { cellCtx, ok := ctx.Row.Current[i] if !ok || !(cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start) { shouldAddSeparator = true } } if shouldAddSeparator { output.WriteString(separatorString) c.logger.Debugf("renderLine: Added separator '%s' before col %d", separatorString, i) } else if i > 0 { c.logger.Debugf("renderLine: Skipped separator before col %d due to HMerge continuation", i) } // Get cell context, use default if not present cellCtx, ok := ctx.Row.Current[i] if !ok { cellCtx = tw.CellContext{ Data: tw.Empty, Align: c.defaultAlign[ctx.Row.Position], Padding: tw.Padding{Left: tw.Space, Right: tw.Space}, Width: ctx.Row.Widths.Get(i), Merge: tw.MergeState{}, } } // Handle merged cells visualWidth := 0 span := 1 isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start if isHMergeStart { span = cellCtx.Merge.Horizontal.Span if ctx.Row.Position == tw.Row { // Calculate dynamic width for row merges dynamicTotalWidth := 0 for k := 0; k < span && i+k < numCols; k++ { colToSum := i + k normWidth := ctx.NormalizedWidths.Get(colToSum) if normWidth < 0 { normWidth = 0 } dynamicTotalWidth += normWidth if k > 0 && separatorDisplayWidth > 0 { dynamicTotalWidth += separatorDisplayWidth } } visualWidth = dynamicTotalWidth c.logger.Debugf("renderLine: Row HMerge col %d, span %d, dynamic visualWidth %d", i, span, visualWidth) } else { visualWidth = ctx.Row.Widths.Get(i) c.logger.Debugf("renderLine: H/F HMerge col %d, span %d, pre-adjusted visualWidth %d", i, span, visualWidth) } } else { visualWidth = ctx.Row.Widths.Get(i) c.logger.Debugf("renderLine: Regular col %d, visualWidth %d", i, visualWidth) } if visualWidth < 0 { visualWidth = 0 } // Skip processing for non-start merged cells if ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start { c.logger.Debugf("renderLine: Skipping col %d processing (part of HMerge)", i) i++ continue } // Handle empty cell context with non-zero width if !ok && visualWidth > 0 { spaces := strings.Repeat(tw.Space, visualWidth) if len(tint.BG) > 0 { output.WriteString(Tint{BG: tint.BG}.Apply(spaces)) } else { output.WriteString(spaces) } c.logger.Debugf("renderLine: No cell context for col %d, writing %d spaces", i, visualWidth) i += span continue } // Set cell alignment padding := cellCtx.Padding align := cellCtx.Align if align == tw.AlignNone { align = c.defaultAlign[ctx.Row.Position] c.logger.Debugf("renderLine: col %d using default renderer align '%s' for position %s because cellCtx.Align was AlignNone", i, align, ctx.Row.Position) } // Detect and handle TOTAL pattern isTotalPattern := false if i == 0 && isHMergeStart && cellCtx.Merge.Horizontal.Span >= 3 && strings.TrimSpace(cellCtx.Data) == "TOTAL" { isTotalPattern = true c.logger.Debugf("renderLine: Detected 'TOTAL' HMerge pattern at col 0") } // Override alignment for footer merges or TOTAL pattern if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern { if align != tw.AlignRight { c.logger.Debugf("renderLine: Applying AlignRight override for Footer HMerge/TOTAL pattern at col %d. Original/default align was: %s", i, align) align = tw.AlignRight } } // Handle vertical/hierarchical merges content := cellCtx.Data if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) || (cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) { content = tw.Empty c.logger.Debugf("renderLine: Blanked data for col %d (non-start V/Hierarchical)", i) } // Apply per-column tint if available cellTint := tint if i < len(tint.Columns) { columnTint := tint.Columns[i] if len(columnTint.FG) > 0 || len(columnTint.BG) > 0 { cellTint = columnTint } } // Format and render the cell formattedCell := c.formatCell(content, visualWidth, padding, align, cellTint) if len(formattedCell) > 0 { output.WriteString(formattedCell) } else if visualWidth == 0 && isHMergeStart { c.logger.Debugf("renderLine: Rendered HMerge START col %d resulted in 0 visual width, wrote nothing.", i) } else if visualWidth == 0 { c.logger.Debugf("renderLine: Rendered regular col %d resulted in 0 visual width, wrote nothing.", i) } // Log rendering details if isHMergeStart { c.logger.Debugf("renderLine: Rendered HMerge START col %d (span %d, visualWidth %d, align %s): '%s'", i, span, visualWidth, align, formattedCell) } else { c.logger.Debugf("renderLine: Rendered regular col %d (visualWidth %d, align %s): '%s'", i, visualWidth, align, formattedCell) } i += span } // Add right border if enabled suffix := tw.Empty if c.config.Borders.Right.Enabled() { suffix = c.config.Border.Apply(c.config.Symbols.Column()) } output.WriteString(suffix) // Write the final line output.WriteString(c.newLine) c.w.Write([]byte(output.String())) c.logger.Debugf("renderLine: Final rendered line: %s", strings.TrimSuffix(output.String(), c.newLine)) } // Rendition updates the parts of ColorizedConfig that correspond to tw.Rendition // by merging the provided newRendition. Color-specific Tints are not modified. func (c *Colorized) Rendition(newRendition tw.Rendition) { // Method name matches interface c.logger.Debug("Colorized.Rendition called. Current B/Sym/Set: B:%+v, Sym:%T, S:%+v. Override: %+v", c.config.Borders, c.config.Symbols, c.config.Settings, newRendition) currentRenditionPart := tw.Rendition{ Borders: c.config.Borders, Symbols: c.config.Symbols, Settings: c.config.Settings, } mergedRenditionPart := mergeRendition(currentRenditionPart, newRendition) c.config.Borders = mergedRenditionPart.Borders c.config.Symbols = mergedRenditionPart.Symbols if c.config.Symbols == nil { c.config.Symbols = tw.NewSymbols(tw.StyleLight) } c.config.Settings = mergedRenditionPart.Settings c.logger.Debugf("Colorized.Rendition updated. New B/Sym/Set: B:%+v, Sym:%T, S:%+v", c.config.Borders, c.config.Symbols, c.config.Settings) } // Ensure Colorized implements tw.Renditioning var _ tw.Renditioning = (*Colorized)(nil) tablewriter-1.0.9/renderer/fn.go000066400000000000000000000155351504170215200166130ustar00rootroot00000000000000package renderer import ( "fmt" "github.com/fatih/color" "github.com/olekukonko/tablewriter/tw" ) // defaultBlueprint returns a default Rendition for ASCII table rendering with borders and light symbols. func defaultBlueprint() tw.Rendition { return tw.Rendition{ Borders: tw.Border{ Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On, }, Settings: tw.Settings{ Separators: tw.Separators{ ShowHeader: tw.On, ShowFooter: tw.On, BetweenRows: tw.Off, BetweenColumns: tw.On, }, Lines: tw.Lines{ ShowTop: tw.On, ShowBottom: tw.On, ShowHeaderLine: tw.On, ShowFooterLine: tw.On, }, CompactMode: tw.Off, // Cushion: tw.On, }, Symbols: tw.NewSymbols(tw.StyleLight), Streaming: true, } } // defaultColorized returns a default ColorizedConfig optimized for dark terminal backgrounds with colored headers, rows, and borders. func defaultColorized() ColorizedConfig { return ColorizedConfig{ Borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On}, Settings: tw.Settings{ Separators: tw.Separators{ ShowHeader: tw.On, ShowFooter: tw.On, BetweenRows: tw.Off, BetweenColumns: tw.On, }, Lines: tw.Lines{ ShowTop: tw.On, ShowBottom: tw.On, ShowHeaderLine: tw.On, ShowFooterLine: tw.On, }, CompactMode: tw.Off, }, Header: Tint{ FG: Colors{color.FgWhite, color.Bold}, BG: Colors{color.BgBlack}, }, Column: Tint{ FG: Colors{color.FgCyan}, BG: Colors{color.BgBlack}, }, Footer: Tint{ FG: Colors{color.FgYellow}, BG: Colors{color.BgBlack}, }, Border: Tint{ FG: Colors{color.FgWhite}, BG: Colors{color.BgBlack}, }, Separator: Tint{ FG: Colors{color.FgWhite}, BG: Colors{color.BgBlack}, }, Symbols: tw.NewSymbols(tw.StyleLight), } } // defaultOceanRendererConfig returns a base tw.Rendition for the Ocean renderer. func defaultOceanRendererConfig() tw.Rendition { return tw.Rendition{ Borders: tw.Border{ Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On, }, Settings: tw.Settings{ Separators: tw.Separators{ ShowHeader: tw.On, ShowFooter: tw.Off, BetweenRows: tw.Off, BetweenColumns: tw.On, }, Lines: tw.Lines{ ShowTop: tw.On, ShowBottom: tw.On, ShowHeaderLine: tw.On, ShowFooterLine: tw.Off, }, CompactMode: tw.Off, }, Symbols: tw.NewSymbols(tw.StyleDefault), Streaming: true, } } // getHTMLStyle remains the same func getHTMLStyle(align tw.Align) string { styleContent := tw.Empty switch align { case tw.AlignRight: styleContent = "text-align: right;" case tw.AlignCenter: styleContent = "text-align: center;" case tw.AlignLeft: styleContent = "text-align: left;" } if styleContent != tw.Empty { return fmt.Sprintf(` style="%s"`, styleContent) } return tw.Empty } // mergeLines combines default and override line settings, preserving defaults for unset (zero) overrides. func mergeLines(defaults, overrides tw.Lines) tw.Lines { if overrides.ShowTop != 0 { defaults.ShowTop = overrides.ShowTop } if overrides.ShowBottom != 0 { defaults.ShowBottom = overrides.ShowBottom } if overrides.ShowHeaderLine != 0 { defaults.ShowHeaderLine = overrides.ShowHeaderLine } if overrides.ShowFooterLine != 0 { defaults.ShowFooterLine = overrides.ShowFooterLine } return defaults } // mergeSeparators combines default and override separator settings, preserving defaults for unset (zero) overrides. func mergeSeparators(defaults, overrides tw.Separators) tw.Separators { if overrides.ShowHeader != 0 { defaults.ShowHeader = overrides.ShowHeader } if overrides.ShowFooter != 0 { defaults.ShowFooter = overrides.ShowFooter } if overrides.BetweenRows != 0 { defaults.BetweenRows = overrides.BetweenRows } if overrides.BetweenColumns != 0 { defaults.BetweenColumns = overrides.BetweenColumns } return defaults } // mergeSettings combines default and override settings, preserving defaults for unset (zero) overrides. func mergeSettings(defaults, overrides tw.Settings) tw.Settings { if overrides.Separators.ShowHeader != tw.Unknown { defaults.Separators.ShowHeader = overrides.Separators.ShowHeader } if overrides.Separators.ShowFooter != tw.Unknown { defaults.Separators.ShowFooter = overrides.Separators.ShowFooter } if overrides.Separators.BetweenRows != tw.Unknown { defaults.Separators.BetweenRows = overrides.Separators.BetweenRows } if overrides.Separators.BetweenColumns != tw.Unknown { defaults.Separators.BetweenColumns = overrides.Separators.BetweenColumns } if overrides.Lines.ShowTop != tw.Unknown { defaults.Lines.ShowTop = overrides.Lines.ShowTop } if overrides.Lines.ShowBottom != tw.Unknown { defaults.Lines.ShowBottom = overrides.Lines.ShowBottom } if overrides.Lines.ShowHeaderLine != tw.Unknown { defaults.Lines.ShowHeaderLine = overrides.Lines.ShowHeaderLine } if overrides.Lines.ShowFooterLine != tw.Unknown { defaults.Lines.ShowFooterLine = overrides.Lines.ShowFooterLine } if overrides.CompactMode != tw.Unknown { defaults.CompactMode = overrides.CompactMode } //if overrides.Cushion != tw.Unknown { // defaults.Cushion = overrides.Cushion //} return defaults } // MergeRendition merges the 'override' rendition into the 'current' rendition. // It only updates fields in 'current' if they are explicitly set (non-zero/non-nil) in 'override'. // This allows for partial updates to a renderer's configuration. func mergeRendition(current, override tw.Rendition) tw.Rendition { // Merge Borders: Only update if override border states are explicitly set (not 0). // A tw.State's zero value is 0, which is distinct from tw.On (1) or tw.Off (-1). // So, if override.Borders.Left is 0, it means "not specified", so we keep current. if override.Borders.Left != 0 { current.Borders.Left = override.Borders.Left } if override.Borders.Right != 0 { current.Borders.Right = override.Borders.Right } if override.Borders.Top != 0 { current.Borders.Top = override.Borders.Top } if override.Borders.Bottom != 0 { current.Borders.Bottom = override.Borders.Bottom } // Merge Symbols: Only update if override.Symbols is not nil. if override.Symbols != nil { current.Symbols = override.Symbols } // Merge Settings: Use the existing mergeSettings for granular control. // mergeSettings already handles preserving defaults for unset (zero) overrides. current.Settings = mergeSettings(current.Settings, override.Settings) // Streaming flag: typically set at renderer creation, but can be overridden if needed. // For now, let's assume it's not commonly changed post-creation by a generic rendition merge. // If override provides a different streaming capability, it might indicate a fundamental // change that a simple merge shouldn't handle without more context. // current.Streaming = override.Streaming // Or keep current.Streaming return current } tablewriter-1.0.9/renderer/html.go000066400000000000000000000304561504170215200171530ustar00rootroot00000000000000package renderer import ( "errors" "fmt" "github.com/olekukonko/ll" "html" "io" "strings" "github.com/olekukonko/tablewriter/tw" ) // HTMLConfig defines settings for the HTML table renderer. type HTMLConfig struct { EscapeContent bool // Whether to escape cell content AddLinesTag bool // Whether to wrap multiline content in tags TableClass string // CSS class for HeaderClass string // CSS class for BodyClass string // CSS class for FooterClass string // CSS class for RowClass string // CSS class for in body HeaderRowClass string // CSS class for in header FooterRowClass string // CSS class for in footer } // HTML renders tables in HTML format with customizable classes and content handling. type HTML struct { config HTMLConfig // Renderer configuration w io.Writer // Output w trace []string // Debug trace messages debug bool // Enables debug logging tableStarted bool // Tracks if
tag is open tbodyStarted bool // Tracks if tag is open tfootStarted bool // Tracks if tag is open vMergeTrack map[int]int // Tracks vertical merge spans by column index logger *ll.Logger } // NewHTML initializes an HTML renderer with the given w, debug setting, and optional configuration. // It panics if the w is nil and applies defaults for unset config fields. // Update: see https://github.com/olekukonko/tablewriter/issues/258 func NewHTML(configs ...HTMLConfig) *HTML { cfg := HTMLConfig{ EscapeContent: true, AddLinesTag: false, } if len(configs) > 0 { userCfg := configs[0] cfg.EscapeContent = userCfg.EscapeContent cfg.AddLinesTag = userCfg.AddLinesTag cfg.TableClass = userCfg.TableClass cfg.HeaderClass = userCfg.HeaderClass cfg.BodyClass = userCfg.BodyClass cfg.FooterClass = userCfg.FooterClass cfg.RowClass = userCfg.RowClass cfg.HeaderRowClass = userCfg.HeaderRowClass cfg.FooterRowClass = userCfg.FooterRowClass } return &HTML{ config: cfg, vMergeTrack: make(map[int]int), tableStarted: false, tbodyStarted: false, tfootStarted: false, logger: ll.New("html"), } } func (h *HTML) Logger(logger *ll.Logger) { h.logger = logger } // Config returns a Rendition representation of the current configuration. func (h *HTML) Config() tw.Rendition { return tw.Rendition{ Borders: tw.BorderNone, Symbols: tw.NewSymbols(tw.StyleNone), Settings: tw.Settings{}, Streaming: false, } } // debugLog appends a formatted message to the debug trace if debugging is enabled. //func (h *HTML) debugLog(format string, a ...interface{}) { // if h.debug { // msg := fmt.Sprintf(format, a...) // h.trace = append(h.trace, fmt.Sprintf("[HTML] %s", msg)) // } //} // Debug returns the accumulated debug trace messages. func (h *HTML) Debug() []string { return h.trace } // Start begins the HTML table rendering by opening the
tag. func (h *HTML) Start(w io.Writer) error { h.w = w h.Reset() h.logger.Debug("HTML.Start() called.") classAttr := tw.Empty if h.config.TableClass != tw.Empty { classAttr = fmt.Sprintf(` class="%s"`, h.config.TableClass) } h.logger.Debugf("Writing opening tag", classAttr) _, err := fmt.Fprintf(h.w, "\n", classAttr) if err != nil { return err } h.tableStarted = true return nil } // closePreviousSection closes any open or sections. func (h *HTML) closePreviousSection() { if h.tbodyStarted { h.logger.Debug("Closing tag") fmt.Fprintln(h.w, "") h.tbodyStarted = false } if h.tfootStarted { h.logger.Debug("Closing tag") fmt.Fprintln(h.w, "") h.tfootStarted = false } } // Header renders the section with header rows, supporting horizontal merges. func (h *HTML) Header(headers [][]string, ctx tw.Formatting) { if !h.tableStarted { h.logger.Debug("WARN: Header called before Start") return } if len(headers) == 0 || len(headers[0]) == 0 { h.logger.Debug("Header: No headers") return } h.closePreviousSection() classAttr := tw.Empty if h.config.HeaderClass != tw.Empty { classAttr = fmt.Sprintf(` class="%s"`, h.config.HeaderClass) } fmt.Fprintf(h.w, "\n", classAttr) headerRow := headers[0] numCols := 0 if len(ctx.Row.Current) > 0 { maxKey := -1 for k := range ctx.Row.Current { if k > maxKey { maxKey = k } } numCols = maxKey + 1 } else if len(headerRow) > 0 { numCols = len(headerRow) } indent := " " rowClassAttr := tw.Empty if h.config.HeaderRowClass != tw.Empty { rowClassAttr = fmt.Sprintf(` class="%s"`, h.config.HeaderRowClass) } fmt.Fprintf(h.w, "%s", indent, rowClassAttr) renderedCols := 0 for colIdx := 0; renderedCols < numCols && colIdx < numCols; { // Skip columns consumed by vertical merges if remainingSpan, merging := h.vMergeTrack[colIdx]; merging && remainingSpan > 1 { h.logger.Debugf("Header: Skipping col %d due to vmerge", colIdx) h.vMergeTrack[colIdx]-- if h.vMergeTrack[colIdx] <= 1 { delete(h.vMergeTrack, colIdx) } colIdx++ continue } // Render cell cellCtx, ok := ctx.Row.Current[colIdx] if !ok { cellCtx = tw.CellContext{Align: tw.AlignCenter} } originalContent := tw.Empty if colIdx < len(headerRow) { originalContent = headerRow[colIdx] } tag, attributes, processedContent := h.renderRowCell(originalContent, cellCtx, true, colIdx) fmt.Fprintf(h.w, "<%s%s>%s", tag, attributes, processedContent, tag) renderedCols++ // Handle horizontal merges hSpan := 1 if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start { hSpan = cellCtx.Merge.Horizontal.Span renderedCols += (hSpan - 1) } colIdx += hSpan } fmt.Fprintf(h.w, "\n") fmt.Fprintln(h.w, "") } // Row renders a element within , supporting horizontal and vertical merges. func (h *HTML) Row(row []string, ctx tw.Formatting) { if !h.tableStarted { h.logger.Debug("WARN: Row called before Start") return } if !h.tbodyStarted { h.closePreviousSection() classAttr := tw.Empty if h.config.BodyClass != tw.Empty { classAttr = fmt.Sprintf(` class="%s"`, h.config.BodyClass) } h.logger.Debugf("Writing opening tag", classAttr) fmt.Fprintf(h.w, "\n", classAttr) h.tbodyStarted = true } h.logger.Debugf("Rendering row data: %v", row) numCols := 0 if len(ctx.Row.Current) > 0 { maxKey := -1 for k := range ctx.Row.Current { if k > maxKey { maxKey = k } } numCols = maxKey + 1 } else if len(row) > 0 { numCols = len(row) } indent := " " rowClassAttr := tw.Empty if h.config.RowClass != tw.Empty { rowClassAttr = fmt.Sprintf(` class="%s"`, h.config.RowClass) } fmt.Fprintf(h.w, "%s", indent, rowClassAttr) renderedCols := 0 for colIdx := 0; renderedCols < numCols && colIdx < numCols; { // Skip columns consumed by vertical merges if remainingSpan, merging := h.vMergeTrack[colIdx]; merging && remainingSpan > 1 { h.logger.Debugf("Row: Skipping render for col %d due to vertical merge (remaining %d)", colIdx, remainingSpan-1) h.vMergeTrack[colIdx]-- if h.vMergeTrack[colIdx] <= 1 { delete(h.vMergeTrack, colIdx) } colIdx++ continue } // Render cell cellCtx, ok := ctx.Row.Current[colIdx] if !ok { cellCtx = tw.CellContext{Align: tw.AlignLeft} } originalContent := tw.Empty if colIdx < len(row) { originalContent = row[colIdx] } tag, attributes, processedContent := h.renderRowCell(originalContent, cellCtx, false, colIdx) fmt.Fprintf(h.w, "<%s%s>%s", tag, attributes, processedContent, tag) renderedCols++ // Handle horizontal merges hSpan := 1 if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start { hSpan = cellCtx.Merge.Horizontal.Span renderedCols += (hSpan - 1) } colIdx += hSpan } fmt.Fprintf(h.w, "\n") } // Footer renders the section with footer rows, supporting horizontal merges. func (h *HTML) Footer(footers [][]string, ctx tw.Formatting) { if !h.tableStarted { h.logger.Debug("WARN: Footer called before Start") return } if len(footers) == 0 || len(footers[0]) == 0 { h.logger.Debug("Footer: No footers") return } h.closePreviousSection() classAttr := tw.Empty if h.config.FooterClass != tw.Empty { classAttr = fmt.Sprintf(` class="%s"`, h.config.FooterClass) } fmt.Fprintf(h.w, "\n", classAttr) h.tfootStarted = true footerRow := footers[0] numCols := 0 if len(ctx.Row.Current) > 0 { maxKey := -1 for k := range ctx.Row.Current { if k > maxKey { maxKey = k } } numCols = maxKey + 1 } else if len(footerRow) > 0 { numCols = len(footerRow) } indent := " " rowClassAttr := tw.Empty if h.config.FooterRowClass != tw.Empty { rowClassAttr = fmt.Sprintf(` class="%s"`, h.config.FooterRowClass) } fmt.Fprintf(h.w, "%s", indent, rowClassAttr) renderedCols := 0 for colIdx := 0; renderedCols < numCols && colIdx < numCols; { cellCtx, ok := ctx.Row.Current[colIdx] if !ok { cellCtx = tw.CellContext{Align: tw.AlignRight} } originalContent := tw.Empty if colIdx < len(footerRow) { originalContent = footerRow[colIdx] } tag, attributes, processedContent := h.renderRowCell(originalContent, cellCtx, false, colIdx) fmt.Fprintf(h.w, "<%s%s>%s", tag, attributes, processedContent, tag) renderedCols++ hSpan := 1 if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start { hSpan = cellCtx.Merge.Horizontal.Span renderedCols += (hSpan - 1) } colIdx += hSpan } fmt.Fprintf(h.w, "\n") fmt.Fprintln(h.w, "") h.tfootStarted = false } // renderRowCell generates HTML for a single cell, handling content escaping, merges, and alignment. func (h *HTML) renderRowCell(originalContent string, cellCtx tw.CellContext, isHeader bool, colIdx int) (tag, attributes, processedContent string) { tag = "td" if isHeader { tag = "th" } // Process content processedContent = originalContent containsNewline := strings.Contains(originalContent, "\n") if h.config.EscapeContent { if containsNewline { const newlinePlaceholder = "[[--HTML_RENDERER_BR_PLACEHOLDER--]]" tempContent := strings.ReplaceAll(originalContent, "\n", newlinePlaceholder) escapedContent := html.EscapeString(tempContent) processedContent = strings.ReplaceAll(escapedContent, newlinePlaceholder, "
") } else { processedContent = html.EscapeString(originalContent) } } else if containsNewline { processedContent = strings.ReplaceAll(originalContent, "\n", "
") } if containsNewline && h.config.AddLinesTag { processedContent = "" + processedContent + "" } // Build attributes var attrBuilder strings.Builder merge := cellCtx.Merge if merge.Horizontal.Present && merge.Horizontal.Start && merge.Horizontal.Span > 1 { fmt.Fprintf(&attrBuilder, ` colspan="%d"`, merge.Horizontal.Span) } vSpan := 0 if !isHeader { if merge.Vertical.Present && merge.Vertical.Start { vSpan = merge.Vertical.Span } else if merge.Hierarchical.Present && merge.Hierarchical.Start { vSpan = merge.Hierarchical.Span } if vSpan > 1 { fmt.Fprintf(&attrBuilder, ` rowspan="%d"`, vSpan) h.vMergeTrack[colIdx] = vSpan h.logger.Debugf("renderRowCell: Tracking rowspan=%d for col %d", vSpan, colIdx) } } if style := getHTMLStyle(cellCtx.Align); style != tw.Empty { attrBuilder.WriteString(style) } attributes = attrBuilder.String() return } // Line is a no-op for HTML rendering, as structural lines are handled by tags. func (h *HTML) Line(ctx tw.Formatting) {} // Reset clears the renderer's internal state, including debug traces and merge tracking. func (h *HTML) Reset() { h.logger.Debug("HTML.Reset() called.") h.tableStarted = false h.tbodyStarted = false h.tfootStarted = false h.vMergeTrack = make(map[int]int) h.trace = nil } // Close ensures all open HTML tags (
, , ) are properly closed. func (h *HTML) Close() error { if h.w == nil { return errors.New("HTML Renderer Close called on nil internal w") } if h.tableStarted { h.logger.Debug("HTML.Close() called.") h.closePreviousSection() h.logger.Debug("Closing
tag.") _, err := fmt.Fprintln(h.w, "
") h.tableStarted = false h.tbodyStarted = false h.tfootStarted = false h.vMergeTrack = make(map[int]int) return err } h.logger.Debug("HTML.Close() called, but table was not started (no-op).") return nil } tablewriter-1.0.9/renderer/junction.go000066400000000000000000000243201504170215200200310ustar00rootroot00000000000000package renderer import ( "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/tw" ) // Junction handles rendering of table junction points (corners, intersections) with color support. type Junction struct { sym tw.Symbols // Symbols used for rendering junctions and lines ctx tw.Formatting // Current table formatting context colIdx int // Index of the column being processed debugging bool // Enables debug logging borderTint Tint // Colors for border symbols separatorTint Tint // Colors for separator symbols logger *ll.Logger } type JunctionContext struct { Symbols tw.Symbols Ctx tw.Formatting ColIdx int Logger *ll.Logger BorderTint Tint SeparatorTint Tint } // NewJunction initializes a Junction with the given symbols, context, and tints. // If debug is nil, a no-op debug function is used. func NewJunction(ctx JunctionContext) *Junction { return &Junction{ sym: ctx.Symbols, ctx: ctx.Ctx, colIdx: ctx.ColIdx, logger: ctx.Logger.Namespace("junction"), borderTint: ctx.BorderTint, separatorTint: ctx.SeparatorTint, } } // getMergeState retrieves the merge state for a specific column in a row, returning an empty state if not found. func (jr *Junction) getMergeState(row map[int]tw.CellContext, colIdx int) tw.MergeState { if row == nil || colIdx < 0 { return tw.MergeState{} } return row[colIdx].Merge } // GetSegment determines whether to render a colored horizontal line or an empty space based on merge states. func (jr *Junction) GetSegment() string { currentMerge := jr.getMergeState(jr.ctx.Row.Current, jr.colIdx) nextMerge := jr.getMergeState(jr.ctx.Row.Next, jr.colIdx) vPassThruStrict := (currentMerge.Vertical.Present && nextMerge.Vertical.Present && !currentMerge.Vertical.End && !nextMerge.Vertical.Start) || (currentMerge.Hierarchical.Present && nextMerge.Hierarchical.Present && !currentMerge.Hierarchical.End && !nextMerge.Hierarchical.Start) if vPassThruStrict { jr.logger.Debugf("GetSegment col %d: VPassThruStrict=%v -> Empty segment", jr.colIdx, vPassThruStrict) return tw.Empty } symbol := jr.sym.Row() coloredSymbol := jr.borderTint.Apply(symbol) jr.logger.Debugf("GetSegment col %d: VPassThruStrict=%v -> Colored row symbol '%s'", jr.colIdx, vPassThruStrict, coloredSymbol) return coloredSymbol } // RenderLeft selects and colors the leftmost junction symbol for the current row line based on position and merges. func (jr *Junction) RenderLeft() string { mergeAbove := jr.getMergeState(jr.ctx.Row.Current, 0) mergeBelow := jr.getMergeState(jr.ctx.Row.Next, 0) jr.logger.Debugf("RenderLeft: Level=%v, Location=%v, Previous=%v", jr.ctx.Level, jr.ctx.Row.Location, jr.ctx.Row.Previous) isTopBorder := (jr.ctx.Level == tw.LevelHeader && jr.ctx.Row.Location == tw.LocationFirst) || (jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationFirst && jr.ctx.Row.Previous == nil) if isTopBorder { symbol := jr.sym.TopLeft() return jr.borderTint.Apply(symbol) } isBottom := jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationEnd && !jr.ctx.HasFooter isFooter := jr.ctx.Level == tw.LevelFooter && jr.ctx.Row.Location == tw.LocationEnd if isBottom || isFooter { symbol := jr.sym.BottomLeft() return jr.borderTint.Apply(symbol) } isVPassThruStrict := (mergeAbove.Vertical.Present && mergeBelow.Vertical.Present && !mergeAbove.Vertical.End && !mergeBelow.Vertical.Start) || (mergeAbove.Hierarchical.Present && mergeBelow.Hierarchical.Present && !mergeAbove.Hierarchical.End && !mergeBelow.Hierarchical.Start) if isVPassThruStrict { symbol := jr.sym.Column() return jr.separatorTint.Apply(symbol) } symbol := jr.sym.MidLeft() return jr.borderTint.Apply(symbol) } // RenderRight selects and colors the rightmost junction symbol for the row line based on position, merges, and last column index. func (jr *Junction) RenderRight(lastColIdx int) string { jr.logger.Debugf("RenderRight: lastColIdx=%d, Level=%v, Location=%v, Previous=%v", lastColIdx, jr.ctx.Level, jr.ctx.Row.Location, jr.ctx.Row.Previous) if lastColIdx < 0 { switch jr.ctx.Level { case tw.LevelHeader: symbol := jr.sym.TopRight() return jr.borderTint.Apply(symbol) case tw.LevelFooter: symbol := jr.sym.BottomRight() return jr.borderTint.Apply(symbol) default: if jr.ctx.Row.Location == tw.LocationFirst { symbol := jr.sym.TopRight() return jr.borderTint.Apply(symbol) } if jr.ctx.Row.Location == tw.LocationEnd { symbol := jr.sym.BottomRight() return jr.borderTint.Apply(symbol) } symbol := jr.sym.MidRight() return jr.borderTint.Apply(symbol) } } mergeAbove := jr.getMergeState(jr.ctx.Row.Current, lastColIdx) mergeBelow := jr.getMergeState(jr.ctx.Row.Next, lastColIdx) isTopBorder := (jr.ctx.Level == tw.LevelHeader && jr.ctx.Row.Location == tw.LocationFirst) || (jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationFirst && jr.ctx.Row.Previous == nil) if isTopBorder { symbol := jr.sym.TopRight() return jr.borderTint.Apply(symbol) } isBottom := jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationEnd && !jr.ctx.HasFooter isFooter := jr.ctx.Level == tw.LevelFooter && jr.ctx.Row.Location == tw.LocationEnd if isBottom || isFooter { symbol := jr.sym.BottomRight() return jr.borderTint.Apply(symbol) } isVPassThruStrict := (mergeAbove.Vertical.Present && mergeBelow.Vertical.Present && !mergeAbove.Vertical.End && !mergeBelow.Vertical.Start) || (mergeAbove.Hierarchical.Present && mergeBelow.Hierarchical.Present && !mergeAbove.Hierarchical.End && !mergeBelow.Hierarchical.Start) if isVPassThruStrict { symbol := jr.sym.Column() return jr.separatorTint.Apply(symbol) } symbol := jr.sym.MidRight() return jr.borderTint.Apply(symbol) } // RenderJunction selects and colors the junction symbol between two adjacent columns based on merge states and table position. func (jr *Junction) RenderJunction(leftColIdx, rightColIdx int) string { mergeCurrentL := jr.getMergeState(jr.ctx.Row.Current, leftColIdx) mergeCurrentR := jr.getMergeState(jr.ctx.Row.Current, rightColIdx) mergeNextL := jr.getMergeState(jr.ctx.Row.Next, leftColIdx) mergeNextR := jr.getMergeState(jr.ctx.Row.Next, rightColIdx) isSpannedCurrent := mergeCurrentL.Horizontal.Present && !mergeCurrentL.Horizontal.End isSpannedNext := mergeNextL.Horizontal.Present && !mergeNextL.Horizontal.End vPassThruLStrict := (mergeCurrentL.Vertical.Present && mergeNextL.Vertical.Present && !mergeCurrentL.Vertical.End && !mergeNextL.Vertical.Start) || (mergeCurrentL.Hierarchical.Present && mergeNextL.Hierarchical.Present && !mergeCurrentL.Hierarchical.End && !mergeNextL.Hierarchical.Start) vPassThruRStrict := (mergeCurrentR.Vertical.Present && mergeNextR.Vertical.Present && !mergeCurrentR.Vertical.End && !mergeNextR.Vertical.Start) || (mergeCurrentR.Hierarchical.Present && mergeNextR.Hierarchical.Present && !mergeCurrentR.Hierarchical.End && !mergeNextR.Hierarchical.Start) isTop := (jr.ctx.Level == tw.LevelHeader && jr.ctx.Row.Location == tw.LocationFirst) || (jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationFirst && len(jr.ctx.Row.Previous) == 0) isBottom := (jr.ctx.Level == tw.LevelFooter && jr.ctx.Row.Location == tw.LocationEnd) || (jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationEnd && !jr.ctx.HasFooter) isPreFooter := jr.ctx.Level == tw.LevelFooter && (jr.ctx.Row.Position == tw.Row || jr.ctx.Row.Position == tw.Header) if isTop { if isSpannedNext { symbol := jr.sym.Row() return jr.borderTint.Apply(symbol) } symbol := jr.sym.TopMid() return jr.borderTint.Apply(symbol) } if isBottom { if vPassThruLStrict && vPassThruRStrict { symbol := jr.sym.Column() return jr.separatorTint.Apply(symbol) } if vPassThruLStrict { symbol := jr.sym.MidLeft() return jr.borderTint.Apply(symbol) } if vPassThruRStrict { symbol := jr.sym.MidRight() return jr.borderTint.Apply(symbol) } if isSpannedCurrent { symbol := jr.sym.Row() return jr.borderTint.Apply(symbol) } symbol := jr.sym.BottomMid() return jr.borderTint.Apply(symbol) } if isPreFooter { if vPassThruLStrict && vPassThruRStrict { symbol := jr.sym.Column() return jr.separatorTint.Apply(symbol) } if vPassThruLStrict { symbol := jr.sym.MidLeft() return jr.borderTint.Apply(symbol) } if vPassThruRStrict { symbol := jr.sym.MidRight() return jr.borderTint.Apply(symbol) } if mergeCurrentL.Horizontal.Present { if !mergeCurrentL.Horizontal.End && mergeCurrentR.Horizontal.Present && !mergeCurrentR.Horizontal.End { jr.logger.Debugf("Footer separator: H-merge continues from col %d to %d (mid-span), using BottomMid", leftColIdx, rightColIdx) symbol := jr.sym.BottomMid() return jr.borderTint.Apply(symbol) } if !mergeCurrentL.Horizontal.End && mergeCurrentR.Horizontal.Present && mergeCurrentR.Horizontal.End { jr.logger.Debugf("Footer separator: H-merge ends at col %d, using BottomMid", rightColIdx) symbol := jr.sym.BottomMid() return jr.borderTint.Apply(symbol) } if mergeCurrentL.Horizontal.End && !mergeCurrentR.Horizontal.Present { jr.logger.Debugf("Footer separator: H-merge ends at col %d, next col %d not merged, using Center", leftColIdx, rightColIdx) symbol := jr.sym.Center() return jr.borderTint.Apply(symbol) } } if isSpannedNext { symbol := jr.sym.BottomMid() return jr.borderTint.Apply(symbol) } if isSpannedCurrent { symbol := jr.sym.TopMid() return jr.borderTint.Apply(symbol) } symbol := jr.sym.Center() return jr.borderTint.Apply(symbol) } if vPassThruLStrict && vPassThruRStrict { symbol := jr.sym.Column() return jr.separatorTint.Apply(symbol) } if vPassThruLStrict { symbol := jr.sym.MidLeft() return jr.borderTint.Apply(symbol) } if vPassThruRStrict { symbol := jr.sym.MidRight() return jr.borderTint.Apply(symbol) } if isSpannedCurrent && isSpannedNext { symbol := jr.sym.Row() return jr.borderTint.Apply(symbol) } if isSpannedCurrent { symbol := jr.sym.TopMid() return jr.borderTint.Apply(symbol) } if isSpannedNext { symbol := jr.sym.BottomMid() return jr.borderTint.Apply(symbol) } symbol := jr.sym.Center() return jr.borderTint.Apply(symbol) } tablewriter-1.0.9/renderer/markdown.go000066400000000000000000000331221504170215200200220ustar00rootroot00000000000000package renderer import ( "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/pkg/twwidth" "io" "strings" "github.com/olekukonko/tablewriter/tw" ) // Markdown renders tables in Markdown format with customizable settings. type Markdown struct { config tw.Rendition // Rendering configuration logger *ll.Logger // Debug trace messages alignment tw.Alignment // alias of []tw.Align w io.Writer } // NewMarkdown initializes a Markdown renderer with defaults tailored for Markdown (e.g., pipes, header separator). // Only the first config is used if multiple are provided. func NewMarkdown(configs ...tw.Rendition) *Markdown { cfg := defaultBlueprint() // Configure Markdown-specific defaults cfg.Symbols = tw.NewSymbols(tw.StyleMarkdown) cfg.Borders = tw.Border{Left: tw.On, Right: tw.On, Top: tw.Off, Bottom: tw.Off} cfg.Settings.Separators.BetweenColumns = tw.On cfg.Settings.Separators.BetweenRows = tw.Off cfg.Settings.Lines.ShowHeaderLine = tw.On cfg.Settings.Lines.ShowTop = tw.Off cfg.Settings.Lines.ShowBottom = tw.Off cfg.Settings.Lines.ShowFooterLine = tw.Off // cfg.Settings.TrimWhitespace = tw.On // Apply user overrides if len(configs) > 0 { cfg = mergeMarkdownConfig(cfg, configs[0]) } return &Markdown{config: cfg, logger: ll.New("markdown")} } // mergeMarkdownConfig combines user-provided config with Markdown defaults, enforcing Markdown-specific settings. func mergeMarkdownConfig(defaults, overrides tw.Rendition) tw.Rendition { if overrides.Borders.Left != 0 { defaults.Borders.Left = overrides.Borders.Left } if overrides.Borders.Right != 0 { defaults.Borders.Right = overrides.Borders.Right } if overrides.Symbols != nil { defaults.Symbols = overrides.Symbols } defaults.Settings = mergeSettings(defaults.Settings, overrides.Settings) // Enforce Markdown requirements defaults.Settings.Lines.ShowHeaderLine = tw.On defaults.Settings.Separators.BetweenColumns = tw.On // defaults.Settings.TrimWhitespace = tw.On return defaults } func (m *Markdown) Logger(logger *ll.Logger) { m.logger = logger.Namespace("markdown") } // Config returns the renderer's current configuration. func (m *Markdown) Config() tw.Rendition { return m.config } // Header renders the Markdown table header and its separator line. func (m *Markdown) Header(headers [][]string, ctx tw.Formatting) { m.resolveAlignment(ctx) if len(headers) == 0 || len(headers[0]) == 0 { m.logger.Debug("Header: No headers to render") return } m.logger.Debugf("Rendering header with %d lines, widths=%v, current=%v, next=%v", len(headers), ctx.Row.Widths, ctx.Row.Current, ctx.Row.Next) // Render header content m.renderMarkdownLine(headers[0], ctx, false) // Render separator if enabled if m.config.Settings.Lines.ShowHeaderLine.Enabled() { sepCtx := ctx sepCtx.Row.Widths = ctx.Row.Widths sepCtx.Row.Current = ctx.Row.Current sepCtx.Row.Previous = ctx.Row.Current sepCtx.IsSubRow = true m.renderMarkdownLine(nil, sepCtx, true) } } // Row renders a Markdown table data row. func (m *Markdown) Row(row []string, ctx tw.Formatting) { m.resolveAlignment(ctx) m.logger.Debugf("Rendering row with data=%v, widths=%v, previous=%v, current=%v, next=%v", row, ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next) m.renderMarkdownLine(row, ctx, false) } // Footer renders the Markdown table footer. func (m *Markdown) Footer(footers [][]string, ctx tw.Formatting) { m.resolveAlignment(ctx) if len(footers) == 0 || len(footers[0]) == 0 { m.logger.Debug("Footer: No footers to render") return } m.logger.Debugf("Rendering footer with %d lines, widths=%v, previous=%v, current=%v, next=%v", len(footers), ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next) m.renderMarkdownLine(footers[0], ctx, false) } // Line is a no-op for Markdown, as only the header separator is rendered (handled by Header). func (m *Markdown) Line(ctx tw.Formatting) { m.logger.Debugf("Line: Generic Line call received (pos: %s, loc: %s). Markdown ignores these.", ctx.Row.Position, ctx.Row.Location) } // Reset clears the renderer's internal state, including debug traces. func (m *Markdown) Reset() { m.logger.Info("Reset: Cleared debug trace") } func (m *Markdown) Start(w io.Writer) error { m.w = w m.logger.Warn("Markdown.Start() called (no-op).") return nil } func (m *Markdown) Close() error { m.logger.Warn("Markdown.Close() called (no-op).") return nil } func (m *Markdown) resolveAlignment(ctx tw.Formatting) tw.Alignment { if len(m.alignment) != 0 { return m.alignment } // get total columns total := len(ctx.Row.Current) // build default alignment for i := 0; i < total; i++ { m.alignment = append(m.alignment, tw.AlignNone) // Default to AlignNone } // add per column alignment if it exists for i := 0; i < total; i++ { m.alignment[i] = ctx.Row.Current[i].Align } m.logger.Debugf(" → Align Resolved %s", m.alignment) return m.alignment } // formatCell formats a Markdown cell's content with padding and alignment, ensuring at least 3 characters wide. func (m *Markdown) formatCell(content string, width int, align tw.Align, padding tw.Padding) string { //if m.config.Settings.TrimWhitespace.Enabled() { // content = strings.TrimSpace(content) //} contentVisualWidth := twwidth.Width(content) // Use specified padding characters or default to spaces padLeftChar := padding.Left if padLeftChar == tw.Empty { padLeftChar = tw.Space } padRightChar := padding.Right if padRightChar == tw.Empty { padRightChar = tw.Space } // Calculate padding widths padLeftCharWidth := twwidth.Width(padLeftChar) padRightCharWidth := twwidth.Width(padRightChar) minWidth := tw.Max(3, contentVisualWidth+padLeftCharWidth+padRightCharWidth) targetWidth := tw.Max(width, minWidth) // Calculate padding totalPaddingNeeded := targetWidth - contentVisualWidth if totalPaddingNeeded < 0 { totalPaddingNeeded = 0 } var leftPadStr, rightPadStr string switch align { case tw.AlignRight: leftPadCount := tw.Max(0, totalPaddingNeeded-padRightCharWidth) rightPadCount := totalPaddingNeeded - leftPadCount leftPadStr = strings.Repeat(padLeftChar, leftPadCount) rightPadStr = strings.Repeat(padRightChar, rightPadCount) case tw.AlignCenter: leftPadCount := totalPaddingNeeded / 2 rightPadCount := totalPaddingNeeded - leftPadCount if leftPadCount < padLeftCharWidth && totalPaddingNeeded >= padLeftCharWidth+padRightCharWidth { leftPadCount = padLeftCharWidth rightPadCount = totalPaddingNeeded - leftPadCount } if rightPadCount < padRightCharWidth && totalPaddingNeeded >= padLeftCharWidth+padRightCharWidth { rightPadCount = padRightCharWidth leftPadCount = totalPaddingNeeded - rightPadCount } leftPadStr = strings.Repeat(padLeftChar, leftPadCount) rightPadStr = strings.Repeat(padRightChar, rightPadCount) default: // AlignLeft rightPadCount := tw.Max(0, totalPaddingNeeded-padLeftCharWidth) leftPadCount := totalPaddingNeeded - rightPadCount leftPadStr = strings.Repeat(padLeftChar, leftPadCount) rightPadStr = strings.Repeat(padRightChar, rightPadCount) } // Build result result := leftPadStr + content + rightPadStr // Adjust width if needed finalWidth := twwidth.Width(result) if finalWidth != targetWidth { m.logger.Debugf("Markdown formatCell MISMATCH: content='%s', target_w=%d, paddingL='%s', paddingR='%s', align=%s -> result='%s', result_w=%d", content, targetWidth, padding.Left, padding.Right, align, result, finalWidth) adjNeeded := targetWidth - finalWidth if adjNeeded > 0 { adjStr := strings.Repeat(tw.Space, adjNeeded) if align == tw.AlignRight { result = adjStr + result } else if align == tw.AlignCenter { leftAdj := adjNeeded / 2 rightAdj := adjNeeded - leftAdj result = strings.Repeat(tw.Space, leftAdj) + result + strings.Repeat(tw.Space, rightAdj) } else { result += adjStr } } else { result = twwidth.Truncate(result, targetWidth) } m.logger.Debugf("Markdown formatCell Corrected: target_w=%d, result='%s', new_w=%d", targetWidth, result, twwidth.Width(result)) } m.logger.Debugf("Markdown formatCell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s' -> '%s' (target %d)", content, width, align, padding.Left, padding.Right, result, targetWidth) return result } // formatSeparator generates a Markdown separator (e.g., `---`, `:--`, `:-:`) with alignment indicators. func (m *Markdown) formatSeparator(width int, align tw.Align) string { targetWidth := tw.Max(3, width) var sb strings.Builder switch align { case tw.AlignLeft: sb.WriteRune(':') sb.WriteString(strings.Repeat("-", targetWidth-1)) case tw.AlignRight: sb.WriteString(strings.Repeat("-", targetWidth-1)) sb.WriteRune(':') case tw.AlignCenter: sb.WriteRune(':') sb.WriteString(strings.Repeat("-", targetWidth-2)) sb.WriteRune(':') case tw.AlignNone: sb.WriteString(strings.Repeat("-", targetWidth)) default: sb.WriteString(strings.Repeat("-", targetWidth)) // Fallback } result := sb.String() currentLen := twwidth.Width(result) if currentLen < targetWidth { result += strings.Repeat("-", targetWidth-currentLen) } else if currentLen > targetWidth { result = twwidth.Truncate(result, targetWidth) } m.logger.Debugf("Markdown formatSeparator: width=%d, align=%s -> '%s'", width, align, result) return result } // renderMarkdownLine renders a single Markdown line (header, row, footer, or separator) with pipes and alignment. func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeaderSep bool) { numCols := 0 if len(ctx.Row.Widths) > 0 { maxKey := -1 for k := range ctx.Row.Widths { if k > maxKey { maxKey = k } } numCols = maxKey + 1 } else if len(ctx.Row.Current) > 0 { maxKey := -1 for k := range ctx.Row.Current { if k > maxKey { maxKey = k } } numCols = maxKey + 1 } else if len(line) > 0 && !isHeaderSep { numCols = len(line) } if numCols == 0 && !isHeaderSep { m.logger.Debug("renderMarkdownLine: Skipping line with zero columns.") return } var output strings.Builder prefix := m.config.Symbols.Column() if m.config.Borders.Left == tw.Off { prefix = tw.Empty } suffix := m.config.Symbols.Column() if m.config.Borders.Right == tw.Off { suffix = tw.Empty } separator := m.config.Symbols.Column() output.WriteString(prefix) colIndex := 0 separatorWidth := twwidth.Width(separator) for colIndex < numCols { cellCtx, ok := ctx.Row.Current[colIndex] align := m.alignment[colIndex] defaultPadding := tw.Padding{Left: tw.Space, Right: tw.Space} if !ok { cellCtx = tw.CellContext{ Data: tw.Empty, Align: align, Padding: defaultPadding, Width: ctx.Row.Widths.Get(colIndex), Merge: tw.MergeState{}, } } else if !cellCtx.Padding.Paddable() { cellCtx.Padding = defaultPadding } // Add separator isContinuation := ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start if colIndex > 0 && !isContinuation { output.WriteString(separator) m.logger.Debugf("renderMarkdownLine: Added separator '%s' before col %d", separator, colIndex) } // Calculate width and span span := 1 visualWidth := 0 isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start if isHMergeStart { span = cellCtx.Merge.Horizontal.Span totalWidth := 0 for k := 0; k < span && colIndex+k < numCols; k++ { colWidth := ctx.NormalizedWidths.Get(colIndex + k) if colWidth < 0 { colWidth = 0 } totalWidth += colWidth if k > 0 && separatorWidth > 0 { totalWidth += separatorWidth } } visualWidth = totalWidth m.logger.Debugf("renderMarkdownLine: HMerge col %d, span %d, visualWidth %d", colIndex, span, visualWidth) } else { visualWidth = ctx.Row.Widths.Get(colIndex) m.logger.Debugf("renderMarkdownLine: Regular col %d, visualWidth %d", colIndex, visualWidth) } if visualWidth < 0 { visualWidth = 0 } // Render segment if isContinuation { m.logger.Debugf("renderMarkdownLine: Skipping col %d (HMerge continuation)", colIndex) } else { var formattedSegment string if isHeaderSep { // Use header's alignment from ctx.Row.Previous headerAlign := align if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK { headerAlign = headerCellCtx.Align // Preserve tw.AlignNone for separator if headerAlign != tw.AlignNone && (headerAlign == tw.Empty || headerAlign == tw.Skip) { headerAlign = tw.AlignCenter } } formattedSegment = m.formatSeparator(visualWidth, headerAlign) } else { content := tw.Empty if colIndex < len(line) { content = line[colIndex] } // For rows, use the header's alignment if specified rowAlign := align if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK && isHeaderSep == false { if headerCellCtx.Align != tw.AlignNone && headerCellCtx.Align != tw.Empty { rowAlign = headerCellCtx.Align } } if rowAlign == tw.AlignNone || rowAlign == tw.Empty { if ctx.Row.Position == tw.Header { rowAlign = tw.AlignCenter } else if ctx.Row.Position == tw.Footer { rowAlign = tw.AlignRight } else { rowAlign = tw.AlignLeft } m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, rowAlign) } formattedSegment = m.formatCell(content, visualWidth, rowAlign, cellCtx.Padding) } output.WriteString(formattedSegment) m.logger.Debugf("renderMarkdownLine: Wrote col %d (span %d, width %d): '%s'", colIndex, span, visualWidth, formattedSegment) } colIndex += span } output.WriteString(suffix) output.WriteString(tw.NewLine) m.w.Write([]byte(output.String())) m.logger.Debugf("renderMarkdownLine: Final line: %s", strings.TrimSuffix(output.String(), tw.NewLine)) } tablewriter-1.0.9/renderer/ocean.go000066400000000000000000000336651504170215200173010ustar00rootroot00000000000000package renderer import ( "github.com/olekukonko/tablewriter/pkg/twwidth" "io" "strings" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/tw" ) // OceanConfig defines configuration specific to the Ocean renderer. type OceanConfig struct { } // Ocean is a streaming table renderer that writes ASCII tables. type Ocean struct { config tw.Rendition oceanConfig OceanConfig fixedWidths tw.Mapper[int, int] widthsFinalized bool tableOutputStarted bool headerContentRendered bool // True if actual header *content* has been rendered by Ocean.Header logger *ll.Logger w io.Writer } func NewOcean(oceanConfig ...OceanConfig) *Ocean { cfg := defaultOceanRendererConfig() oCfg := OceanConfig{} if len(oceanConfig) > 0 { // Apply user-provided OceanConfig if necessary } r := &Ocean{ config: cfg, oceanConfig: oCfg, fixedWidths: tw.NewMapper[int, int](), logger: ll.New("ocean"), } r.resetState() return r } func (o *Ocean) resetState() { o.fixedWidths = tw.NewMapper[int, int]() o.widthsFinalized = false o.tableOutputStarted = false o.headerContentRendered = false o.logger.Debug("State reset.") } func (o *Ocean) Logger(logger *ll.Logger) { o.logger = logger.Namespace("ocean") } func (o *Ocean) Config() tw.Rendition { return o.config } func (o *Ocean) tryFinalizeWidths(ctx tw.Formatting) { if o.widthsFinalized { return } if ctx.Row.Widths != nil && ctx.Row.Widths.Len() > 0 { o.fixedWidths = ctx.Row.Widths.Clone() o.widthsFinalized = true o.logger.Debugf("Widths finalized from context: %v", o.fixedWidths) } else { o.logger.Warn("Attempted to finalize widths, but no width data in context.") } } func (o *Ocean) Start(w io.Writer) error { o.w = w o.logger.Debug("Start() called.") o.resetState() // Top border is drawn by the first component (Header or Row) that has widths // OR by an explicit Line() call from table.go's batch renderer. return nil } // renderTopBorderIfNeeded is called by Header or Row if it's the first to render // and tableOutputStarted is false. func (o *Ocean) renderTopBorderIfNeeded(currentPosition tw.Position, ctx tw.Formatting) { if !o.tableOutputStarted && o.widthsFinalized { // This renderer's config for Top border if o.config.Borders.Top.Enabled() && o.config.Settings.Lines.ShowTop.Enabled() { o.logger.Debugf("Ocean itself rendering top border (triggered by %s)", currentPosition) lineCtx := tw.Formatting{ // Construct specific context for this line Row: tw.RowContext{ Widths: o.fixedWidths, Location: tw.LocationFirst, Position: currentPosition, Next: ctx.Row.Current, // The actual first content is "Next" to the top border }, Level: tw.LevelHeader, } o.Line(lineCtx) o.tableOutputStarted = true } } } func (o *Ocean) Header(headers [][]string, ctx tw.Formatting) { o.logger.Debugf("Ocean.Header called: IsSubRow=%v, Location=%v, NumLines=%d", ctx.IsSubRow, ctx.Row.Location, len(headers)) if !o.widthsFinalized { o.tryFinalizeWidths(ctx) } if !o.widthsFinalized { o.logger.Error("Ocean.Header: Cannot render content, widths are not finalized.") return } if len(headers) > 0 && len(headers[0]) > 0 { for i, headerLineData := range headers { currentLineCtx := ctx currentLineCtx.Row.Widths = o.fixedWidths if i > 0 { currentLineCtx.IsSubRow = true } o.renderContentLine(currentLineCtx, headerLineData) o.tableOutputStarted = true // Content was written } o.headerContentRendered = true } else { o.logger.Debug("Ocean.Header: No actual header content lines to render.") } } func (o *Ocean) Row(row []string, ctx tw.Formatting) { o.logger.Debugf("Ocean.Row called: IsSubRow=%v, Location=%v, DataItems=%d", ctx.IsSubRow, ctx.Row.Location, len(row)) if !o.widthsFinalized { o.tryFinalizeWidths(ctx) } if !o.widthsFinalized { o.logger.Error("Ocean.Row: Cannot render content, widths are not finalized.") return } ctx.Row.Widths = o.fixedWidths o.renderContentLine(ctx, row) o.tableOutputStarted = true } func (o *Ocean) Footer(footers [][]string, ctx tw.Formatting) { o.logger.Debugf("Ocean.Footer called: IsSubRow=%v, Location=%v, NumLines=%d", ctx.IsSubRow, ctx.Row.Location, len(footers)) if !o.widthsFinalized { o.tryFinalizeWidths(ctx) o.logger.Warn("Ocean.Footer: Widths finalized at Footer stage (unusual).") } if !o.widthsFinalized { o.logger.Error("Ocean.Footer: Cannot render content, widths are not finalized.") return } if len(footers) > 0 && len(footers[0]) > 0 { for i, footerLineData := range footers { currentLineCtx := ctx currentLineCtx.Row.Widths = o.fixedWidths if i > 0 { currentLineCtx.IsSubRow = true } o.renderContentLine(currentLineCtx, footerLineData) o.tableOutputStarted = true } } else { o.logger.Debug("Ocean.Footer: No actual footer content lines to render.") } } func (o *Ocean) Line(ctx tw.Formatting) { if !o.widthsFinalized { o.tryFinalizeWidths(ctx) if !o.widthsFinalized { o.logger.Error("Ocean.Line: Called but widths could not be finalized. Skipping line rendering.") return } } // Ensure Line uses the consistent fixedWidths for drawing ctx.Row.Widths = o.fixedWidths o.logger.Debugf("Ocean.Line DRAWING: Level=%v, Loc=%s, Pos=%s, IsSubRow=%t, WidthsLen=%d", ctx.Level, ctx.Row.Location, ctx.Row.Position, ctx.IsSubRow, ctx.Row.Widths.Len()) jr := NewJunction(JunctionContext{ Symbols: o.config.Symbols, Ctx: ctx, ColIdx: 0, Logger: o.logger, BorderTint: Tint{}, SeparatorTint: Tint{}, }) var line strings.Builder sortedColIndices := o.fixedWidths.SortedKeys() if len(sortedColIndices) == 0 { drewEmptyBorders := false if o.config.Borders.Left.Enabled() { line.WriteString(jr.RenderLeft()) drewEmptyBorders = true } if o.config.Borders.Right.Enabled() { line.WriteString(jr.RenderRight(-1)) drewEmptyBorders = true } if drewEmptyBorders { line.WriteString(tw.NewLine) o.w.Write([]byte(line.String())) o.logger.Debug("Line: Drew empty table borders based on Line call.") } else { o.logger.Debug("Line: Handled empty table case (no columns, no borders).") } o.tableOutputStarted = drewEmptyBorders // A line counts as output return } if o.config.Borders.Left.Enabled() { line.WriteString(jr.RenderLeft()) } for i, colIdx := range sortedColIndices { jr.colIdx = colIdx segmentChar := jr.GetSegment() colVisualWidth := o.fixedWidths.Get(colIdx) if colVisualWidth <= 0 { // Still need to consider separators after zero-width columns } else { if segmentChar == tw.Empty { segmentChar = o.config.Symbols.Row() } segmentDisplayWidth := twwidth.Width(segmentChar) if segmentDisplayWidth <= 0 { segmentDisplayWidth = 1 } repeatCount := 0 if colVisualWidth > 0 { repeatCount = colVisualWidth / segmentDisplayWidth if repeatCount == 0 { repeatCount = 1 } } line.WriteString(strings.Repeat(segmentChar, repeatCount)) } if i < len(sortedColIndices)-1 && o.config.Settings.Separators.BetweenColumns.Enabled() { nextColIdx := sortedColIndices[i+1] line.WriteString(jr.RenderJunction(colIdx, nextColIdx)) } } if o.config.Borders.Right.Enabled() { lastColIdx := sortedColIndices[len(sortedColIndices)-1] line.WriteString(jr.RenderRight(lastColIdx)) } line.WriteString(tw.NewLine) o.w.Write([]byte(line.String())) o.tableOutputStarted = true o.logger.Debugf("Line rendered by explicit call: %s", strings.TrimSuffix(line.String(), tw.NewLine)) } func (o *Ocean) Close() error { o.logger.Debug("Ocean.Close() called.") o.resetState() return nil } func (o *Ocean) renderContentLine(ctx tw.Formatting, lineData []string) { if !o.widthsFinalized || o.fixedWidths.Len() == 0 { o.logger.Error("renderContentLine: Cannot render, fixedWidths not set or empty.") return } var output strings.Builder if o.config.Borders.Left.Enabled() { output.WriteString(o.config.Symbols.Column()) } sortedColIndices := o.fixedWidths.SortedKeys() for i, colIdx := range sortedColIndices { cellVisualWidth := o.fixedWidths.Get(colIdx) cellContent := tw.Empty align := tw.AlignDefault padding := tw.Padding{Left: tw.Space, Right: tw.Space} switch ctx.Row.Position { case tw.Header: align = tw.AlignCenter case tw.Footer: align = tw.AlignRight default: align = tw.AlignLeft } cellCtx, hasCellCtx := ctx.Row.Current[colIdx] if hasCellCtx { cellContent = cellCtx.Data if cellCtx.Align.Validate() == nil && cellCtx.Align != tw.AlignNone { align = cellCtx.Align } if cellCtx.Padding.Paddable() { padding = cellCtx.Padding } } else if colIdx < len(lineData) { cellContent = lineData[colIdx] } actualCellWidthToRender := cellVisualWidth isHMergeContinuation := false if hasCellCtx && cellCtx.Merge.Horizontal.Present { if cellCtx.Merge.Horizontal.Start { hSpan := cellCtx.Merge.Horizontal.Span if hSpan <= 0 { hSpan = 1 } currentMergeTotalRenderWidth := 0 for k := 0; k < hSpan; k++ { idxInMergeSpan := colIdx + k // Check if idxInMergeSpan is a defined column in fixedWidths foundInFixedWidths := false for _, sortedCIdx_inner := range sortedColIndices { if sortedCIdx_inner == idxInMergeSpan { currentMergeTotalRenderWidth += o.fixedWidths.Get(idxInMergeSpan) foundInFixedWidths = true break } } if !foundInFixedWidths && idxInMergeSpan <= sortedColIndices[len(sortedColIndices)-1] { o.logger.Debugf("Col %d in HMerge span not found in fixedWidths, assuming 0-width contribution.", idxInMergeSpan) } if k < hSpan-1 && o.config.Settings.Separators.BetweenColumns.Enabled() { currentMergeTotalRenderWidth += twwidth.Width(o.config.Symbols.Column()) } } actualCellWidthToRender = currentMergeTotalRenderWidth } else { isHMergeContinuation = true } } if isHMergeContinuation { o.logger.Debugf("renderContentLine: Col %d is HMerge continuation, skipping content render.", colIdx) // The separator logic below needs to handle this correctly. // If the *previous* column was the start of a merge that spans *this* column, // then the separator after the previous column should have been suppressed. } else if actualCellWidthToRender > 0 { formattedCell := o.formatCellContent(cellContent, actualCellWidthToRender, padding, align) output.WriteString(formattedCell) } else { o.logger.Debugf("renderContentLine: col %d has 0 render width, writing no content.", colIdx) } // Add column separator if: // 1. It's not the last column in sortedColIndices // 2. Separators are enabled // 3. This cell is NOT a horizontal merge start that spans over the next column. if i < len(sortedColIndices)-1 && o.config.Settings.Separators.BetweenColumns.Enabled() { shouldAddSeparator := true if hasCellCtx && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start { // If this merge start spans beyond the current colIdx into the next sortedColIndex if colIdx+cellCtx.Merge.Horizontal.Span > sortedColIndices[i+1] { shouldAddSeparator = false // Separator is part of the merged cell's width o.logger.Debugf("renderContentLine: Suppressed separator after HMerge col %d.", colIdx) } } if shouldAddSeparator { output.WriteString(o.config.Symbols.Column()) } } } if o.config.Borders.Right.Enabled() { output.WriteString(o.config.Symbols.Column()) } output.WriteString(tw.NewLine) o.w.Write([]byte(output.String())) o.logger.Debugf("Content line rendered: %s", strings.TrimSuffix(output.String(), tw.NewLine)) } func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding tw.Padding, align tw.Align) string { if cellVisualWidth <= 0 { return tw.Empty } contentDisplayWidth := twwidth.Width(content) padLeftChar := padding.Left if padLeftChar == tw.Empty { padLeftChar = tw.Space } padRightChar := padding.Right if padRightChar == tw.Empty { padRightChar = tw.Space } padLeftDisplayWidth := twwidth.Width(padLeftChar) padRightDisplayWidth := twwidth.Width(padRightChar) spaceForContentAndAlignment := cellVisualWidth - padLeftDisplayWidth - padRightDisplayWidth if spaceForContentAndAlignment < 0 { spaceForContentAndAlignment = 0 } if contentDisplayWidth > spaceForContentAndAlignment { content = twwidth.Truncate(content, spaceForContentAndAlignment) contentDisplayWidth = twwidth.Width(content) } remainingSpace := spaceForContentAndAlignment - contentDisplayWidth if remainingSpace < 0 { remainingSpace = 0 } var PL, PR string switch align { case tw.AlignRight: PL = strings.Repeat(tw.Space, remainingSpace) case tw.AlignCenter: leftSpaces := remainingSpace / 2 rightSpaces := remainingSpace - leftSpaces PL = strings.Repeat(tw.Space, leftSpaces) PR = strings.Repeat(tw.Space, rightSpaces) default: PR = strings.Repeat(tw.Space, remainingSpace) } var sb strings.Builder sb.WriteString(padLeftChar) sb.WriteString(PL) sb.WriteString(content) sb.WriteString(PR) sb.WriteString(padRightChar) currentFormattedWidth := twwidth.Width(sb.String()) if currentFormattedWidth < cellVisualWidth { if align == tw.AlignRight { prefixSpaces := strings.Repeat(tw.Space, cellVisualWidth-currentFormattedWidth) finalStr := prefixSpaces + sb.String() sb.Reset() sb.WriteString(finalStr) } else { sb.WriteString(strings.Repeat(tw.Space, cellVisualWidth-currentFormattedWidth)) } } else if currentFormattedWidth > cellVisualWidth { tempStr := sb.String() sb.Reset() sb.WriteString(twwidth.Truncate(tempStr, cellVisualWidth)) o.logger.Warnf("formatCellContent: Final string '%s' (width %d) exceeded target %d. Force truncated.", tempStr, currentFormattedWidth, cellVisualWidth) } return sb.String() } func (o *Ocean) Rendition(config tw.Rendition) { o.config = mergeRendition(o.config, config) o.logger.Debugf("Blueprint.Rendition updated. New internal config: %+v", o.config) } // Ensure Blueprint implements tw.Renditioning var _ tw.Renditioning = (*Ocean)(nil) tablewriter-1.0.9/renderer/svg.go000066400000000000000000000562221504170215200170050ustar00rootroot00000000000000package renderer import ( "fmt" "github.com/olekukonko/ll" "html" "io" "strings" "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, ``, 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, ``, totalWidth, totalHeight, html.EscapeString(s.config.FontFamily), s.config.FontSize) fmt.Fprintln(s.w) fmt.Fprintln(s.w, "") if _, err := io.WriteString(s.w, s.svgElements.String()); err != nil { fmt.Fprintln(s.w, ``) return fmt.Errorf("failed to write SVG elements: %w", err) } if s.maxCols > 0 || s.numVisualRowsDrawn > 0 { fmt.Fprintf(s.w, ` `, 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, ` %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, ` %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, " ") } fmt.Fprintln(s.w, ``) 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 := s.dataRowCounter - 1 if parentDataRowStripeIndex < 0 { parentDataRowStripeIndex = 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, ` %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 if cellTextAnchor == "middle" { textX = currentX + s.config.Padding + (rectWidth-2*s.config.Padding)/2.0 } else if cellTextAnchor == "end" { textX = currentX + rectWidth - s.config.Padding } textY := s.currentY + rectHeight/2.0 escapedCell := html.EscapeString(textToRender) fmt.Fprintf(&s.svgElements, ` %s%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, fmt.Sprintf("[SVG] %s", 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) } tablewriter-1.0.9/stream.go000066400000000000000000001405731504170215200156760ustar00rootroot00000000000000package tablewriter import ( "github.com/olekukonko/errors" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" "math" ) // Close finalizes the table stream. // It requires the stream to be started (by calling NewStreamTable). // It calls the renderer's Close method to render final elements (like the bottom border) and close the stream. func (t *Table) Close() error { t.logger.Debug("Close() called. Finalizing stream.") // Ensure stream was actually started and enabled if !t.config.Stream.Enable || !t.hasPrinted { t.logger.Warn("Close() called but streaming not enabled or not started. Ignoring Close() actions.") // If renderer has a Close method that should always be called, consider that. // For Blueprint, Close is a no-op, so returning early is fine. // If we always call renderer.Close(), ensure it's safe if renderer.Start() wasn't called. // Let's only call renderer.Close if stream was started. if t.hasPrinted && t.renderer != nil { // Check if renderer is not nil for safety t.renderer.Close() // Still call renderer's close for cleanup } t.hasPrinted = false // Reset flag return nil } // Render stored footer if any if len(t.streamFooterLines) > 0 { t.logger.Debug("Close(): Rendering stored footer.") if err := t.streamRenderFooter(t.streamFooterLines); err != nil { t.logger.Errorf("Close(): Failed to render stream footer: %v", err) // Continue to try and close renderer and render bottom border } } // Render the final table bottom border t.logger.Debug("Close(): Rendering stream bottom border.") if err := t.streamRenderBottomBorder(); err != nil { t.logger.Errorf("Close(): Failed to render stream bottom border: %v", err) // Continue to try and close renderer } // Call the underlying renderer's Close method err := t.renderer.Close() if err != nil { t.logger.Errorf("Renderer.Close() failed: %v", err) } // Reset streaming state t.hasPrinted = false t.headerRendered = false t.firstRowRendered = false t.lastRenderedLineContent = nil t.lastRenderedMergeState = nil t.lastRenderedPosition = "" t.streamFooterLines = nil // t.streamWidths should persist if we want to make multiple Start/Close calls on same config? // For now, let's assume Start re-evaluates. If widths are from StreamConfig, they'd be reused. // If derived, they'd be re-derived. Let's clear for true reset. t.streamWidths = tw.NewMapper[int, int]() t.streamNumCols = 0 // t.streamRowCounter = 0 // Removed this field t.logger.Debug("Stream ended. hasPrinted = false.") return err // Return error from renderer.Close or other significant errors } // Start initializes the table stream. // In this streaming model, renderer.Start() is primarily called in NewStreamTable. // This method serves as a safeguard or point for adding pre-rendering logic. // Start initializes the table stream. // It is the entry point for streaming mode. // Requires t.config.Stream.Enable to be true. // Returns an error if streaming is disabled or the renderer does not support streaming, // or if called multiple times on the same stream. func (t *Table) Start() error { t.ensureInitialized() // Ensures basic setup like loggers if !t.config.Stream.Enable { // Start() should only be called when streaming is explicitly enabled. // Otherwise, the user should call Render() for batch mode. t.logger.Warn("Start() called but streaming is disabled. Call Render() instead for batch mode.") return errors.New("start() called but streaming is disabled") } if !t.renderer.Config().Streaming { // Check if the configured renderer actually supports streaming. t.logger.Error("Configured renderer does not support streaming.") return errors.Newf("renderer does not support streaming") } //t.renderer.Start(t.writer) //t.renderer.Logger(t.logger) if t.hasPrinted { // Prevent calling Start() multiple times on the same stream instance. t.logger.Warn("Start() called multiple times for the same table stream. Ignoring subsequent calls.") return nil } t.logger.Debug("Starting table stream.") // Initialize/reset streaming state flags and buffers t.headerRendered = false t.firstRowRendered = false t.lastRenderedLineContent = nil t.lastRenderedPosition = "" // Reset last rendered position t.streamFooterLines = nil // Reset footer buffer t.streamNumCols = 0 // Reset derived column count // Calculate initial fixed widths if provided in StreamConfig.Widths // These widths will be used for all subsequent rendering in streaming mode. if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 { // Use per-column stream widths if set t.logger.Debugf("Using per-column stream widths from StreamConfig: %v", t.config.Widths.PerColumn) t.streamWidths = t.config.Widths.PerColumn.Clone() // Determine numCols from the highest index in PerColumn map maxColIdx := -1 t.streamWidths.Each(func(col int, width int) { if col > maxColIdx { maxColIdx = col } // Ensure configured widths are reasonable (>0 becomes >=1, <0 becomes 0) if width > 0 && width < 1 { t.streamWidths.Set(col, 1) } else if width < 0 { t.streamWidths.Set(col, 0) // Negative width means hide column } }) if maxColIdx >= 0 { t.streamNumCols = maxColIdx + 1 t.logger.Debugf("Derived streamNumCols from PerColumn widths: %d", t.streamNumCols) } else { // PerColumn map exists but is empty? Or all negative widths? Assume 0 columns for now. t.streamNumCols = 0 t.logger.Debugf("PerColumn widths map is effectively empty or contains only negative values, streamNumCols = 0.") } } else if t.config.Widths.Global > 0 { // Global width is set, but we don't know the number of columns yet. // Defer applying global width until the first data (Header or first Row) arrives. // Store a placeholder or flag indicating global width should be used. // The simple way for now: Keep streamWidths empty, signal the global width preference. // The width calculation function called later will need to check StreamConfig.Widths.Global // if streamWidths is empty. t.logger.Debugf("Global stream width %d set in StreamConfig. Will derive numCols from first data.", t.config.Widths.Global) t.streamWidths = tw.NewMapper[int, int]() // Initialize as empty, will be populated later // Note: No need to store Global width value here, it's available in t.config.Stream.Widths.Global } else { // No explicit stream widths in config. They will be calculated from the first data (Header or first Row). t.logger.Debug("No explicit stream widths configured in StreamConfig. Will derive from first data.") t.streamWidths = tw.NewMapper[int, int]() // Initialize as empty, will be populated later t.streamNumCols = 0 // NumCols will be determined by first data } // Log warnings if incompatible features are enabled in streaming config // Vertical/Hierarchical merges require processing all rows together. if t.config.Header.Formatting.MergeMode&(tw.MergeVertical|tw.MergeHierarchical) != 0 { t.logger.Warnf("Vertical or Hierarchical merge modes enabled on Header config (%d) but are unsupported in streaming mode. Only Horizontal merge will be considered.", t.config.Header.Formatting.MergeMode) } if t.config.Row.Formatting.MergeMode&(tw.MergeVertical|tw.MergeHierarchical) != 0 { t.logger.Warnf("Vertical or Hierarchical merge modes enabled on Row config (%d) but are unsupported in streaming mode. Only Horizontal merge will be considered.", t.config.Row.Formatting.MergeMode) } if t.config.Footer.Formatting.MergeMode&(tw.MergeVertical|tw.MergeHierarchical) != 0 { t.logger.Warnf("Vertical or Hierarchical merge modes enabled on Footer config (%d) but are unsupported in streaming mode. Only Horizontal merge will be considered.", t.config.Footer.Formatting.MergeMode) } // AutoHide requires processing all row data to find empty columns. if t.config.Behavior.AutoHide.Enabled() { t.logger.Warn("AutoHide is enabled in config but is ignored in streaming mode.") } // Call the renderer's start method for the stream. err := t.renderer.Start(t.writer) if err == nil { t.hasPrinted = true // Mark as started successfully only if renderer.Start works t.logger.Debug("Renderer.Start() succeeded. Table stream initiated.") } else { // Reset state if renderer.Start fails t.hasPrinted = false t.headerRendered = false t.firstRowRendered = false t.lastRenderedLineContent = nil t.lastRenderedPosition = "" t.streamFooterLines = nil t.streamWidths = tw.NewMapper[int, int]() // Clear any widths that might have been set t.streamNumCols = 0 t.logger.Errorf("Renderer.Start() failed: %v. Streaming initialization failed.", err) } return err } // streamAppendRow processes and renders a single row in streaming mode. // It calculates/uses fixed stream widths, processes content, renders separators and lines, // and updates streaming state. // It assumes Start() has already been called and t.hasPrinted is true. func (t *Table) streamAppendRow(row interface{}) error { t.logger.Debugf("streamAppendRow called with row: %v (type: %T)", row, row) if !t.config.Stream.Enable { return errors.New("streaming mode is disabled") } rawCellsSlice, err := t.convertCellsToStrings(row, t.config.Row) if err != nil { t.logger.Errorf("streamAppendRow: Failed to convert row to strings: %v", err) return errors.Newf("failed to convert row to strings").Wrap(err) } if len(rawCellsSlice) == 0 { t.logger.Debug("streamAppendRow: No raw cells after conversion, skipping row rendering.") if !t.firstRowRendered { t.firstRowRendered = true t.logger.Debug("streamAppendRow: Marked first row rendered (empty content after processing).") } return nil } if err := t.ensureStreamWidthsCalculated(rawCellsSlice, t.config.Row); err != nil { return errors.New("failed to establish stream column count/widths").Wrap(err) } // Now, check for column mismatch if a column count has been established. if t.streamNumCols > 0 { if len(rawCellsSlice) != t.streamNumCols { if t.config.Stream.StrictColumns { err := errors.Newf("input row column count (%d) does not match established stream column count (%d) and StrictColumns is enabled", len(rawCellsSlice), t.streamNumCols) t.logger.Error(err.Error()) return err } // If not strict, retain the old lenient behavior (warn and pad/truncate) t.logger.Warnf("streamAppendRow: Input row column count (%d) != stream column count (%d). Padding/Truncating (StrictColumns is false).", len(rawCellsSlice), t.streamNumCols) if len(rawCellsSlice) < t.streamNumCols { paddedCells := make([]string, t.streamNumCols) copy(paddedCells, rawCellsSlice) for i := len(rawCellsSlice); i < t.streamNumCols; i++ { paddedCells[i] = tw.Empty } rawCellsSlice = paddedCells } else { rawCellsSlice = rawCellsSlice[:t.streamNumCols] } } } else if len(rawCellsSlice) > 0 && t.config.Stream.StrictColumns { err := errors.Newf("failed to establish stream column count from first data row (%d cells) and StrictColumns is enabled", len(rawCellsSlice)) t.logger.Error(err.Error()) return err } if t.streamNumCols == 0 { t.logger.Warn("streamAppendRow: streamNumCols is 0. Cannot render row.") return errors.New("cannot render row, column count is zero and could not be determined") } _, rowMerges, _ := t.prepareWithMerges([][]string{rawCellsSlice}, t.config.Row, tw.Row) processedRowLines := t.prepareContent(rawCellsSlice, t.config.Row) t.logger.Debugf("streamAppendRow: Processed row lines: %d lines", len(processedRowLines)) f := t.renderer cfg := t.renderer.Config() if !t.headerRendered && !t.firstRowRendered && t.lastRenderedPosition == "" { if cfg.Borders.Top.Enabled() && cfg.Settings.Lines.ShowTop.Enabled() { t.logger.Debug("streamAppendRow: Rendering table top border (first element is a row).") var nextCellsCtx map[int]tw.CellContext if len(processedRowLines) > 0 { firstRowLineResp := t.streamBuildCellContexts( tw.Row, 0, 0, processedRowLines, rowMerges, t.config.Row, ) nextCellsCtx = firstRowLineResp.cells } f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Next: nextCellsCtx, Position: tw.Row, Location: tw.LocationFirst, }, Level: tw.LevelHeader, IsSubRow: false, NormalizedWidths: t.streamWidths, }) t.logger.Debug("streamAppendRow: Top border rendered.") } } shouldDrawHeaderRowSeparator := t.headerRendered && !t.firstRowRendered && cfg.Settings.Lines.ShowHeaderLine.Enabled() shouldDrawRowRowSeparator := t.firstRowRendered && cfg.Settings.Separators.BetweenRows.Enabled() firstCellForLog := "" if len(rawCellsSlice) > 0 { firstCellForLog = rawCellsSlice[0] } t.logger.Debugf("streamAppendRow: Separator Pre-Check for row starting with '%s': headerRendered=%v, firstRowRendered=%v, ShowHeaderLine=%v, BetweenRows=%v, lastRenderedPos=%q", firstCellForLog, t.headerRendered, t.firstRowRendered, cfg.Settings.Lines.ShowHeaderLine.Enabled(), cfg.Settings.Separators.BetweenRows.Enabled(), t.lastRenderedPosition) t.logger.Debugf("streamAppendRow: Separator Decision Flags for row starting with '%s': shouldDrawHeaderRowSeparator=%v, shouldDrawRowRowSeparator=%v", firstCellForLog, shouldDrawHeaderRowSeparator, shouldDrawRowRowSeparator) if (shouldDrawHeaderRowSeparator || shouldDrawRowRowSeparator) && t.lastRenderedPosition != tw.Position("separator") { t.logger.Debugf("streamAppendRow: Rendering separator line for row starting with '%s'.", firstCellForLog) prevCellsCtx := t.streamRenderedMergeState(t.lastRenderedLineContent, t.lastRenderedMergeState) var nextCellsCtx map[int]tw.CellContext if len(processedRowLines) > 0 { firstRowLineResp := t.streamBuildCellContexts(tw.Row, 0, 0, processedRowLines, rowMerges, t.config.Row) nextCellsCtx = firstRowLineResp.cells } f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Current: prevCellsCtx, Previous: nil, Next: nextCellsCtx, Position: tw.Row, Location: tw.LocationMiddle, }, Level: tw.LevelBody, IsSubRow: false, NormalizedWidths: t.streamWidths, }) t.lastRenderedPosition = tw.Position("separator") t.lastRenderedLineContent = nil t.lastRenderedMergeState = nil t.logger.Debug("streamAppendRow: Separator line rendered. Updated lastRenderedPosition to 'separator'.") } else { details := "" if !(shouldDrawHeaderRowSeparator || shouldDrawRowRowSeparator) { details = "neither header/row nor row/row separator was flagged true" } else if t.lastRenderedPosition == tw.Position("separator") { details = "lastRenderedPosition is already 'separator'" } else { details = "an unexpected combination of conditions" } t.logger.Debugf("streamAppendRow: Separator not drawn for row '%s' because %s.", firstCellForLog, details) } if len(processedRowLines) == 0 { t.logger.Debugf("streamAppendRow: No processed row lines to render for row starting with '%s'.", firstCellForLog) if !t.firstRowRendered { t.firstRowRendered = true t.logger.Debugf("streamAppendRow: Marked first row rendered (empty content after processing).") } return nil } totalRowLines := len(processedRowLines) for i := 0; i < totalRowLines; i++ { resp := t.streamBuildCellContexts(tw.Row, 0, i, processedRowLines, rowMerges, t.config.Row) t.logger.Debug("streamAppendRow: Rendering row line %d/%d with location %v for row starting with '%s'.", i, totalRowLines, resp.location, firstCellForLog) f.Row(resp.cellsContent, tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Current: resp.cells, Previous: resp.prevCells, Next: resp.nextCells, Position: tw.Row, Location: resp.location, }, Level: tw.LevelBody, IsSubRow: i > 0, NormalizedWidths: t.streamWidths, HasFooter: len(t.streamFooterLines) > 0, }) t.lastRenderedLineContent = resp.cellsContent t.lastRenderedMergeState = make(map[int]tw.MergeState) for colIdx, cellCtx := range resp.cells { t.lastRenderedMergeState[colIdx] = cellCtx.Merge } t.lastRenderedPosition = tw.Row } if !t.firstRowRendered { t.firstRowRendered = true t.logger.Debug("streamAppendRow: Marked first row rendered (after processing content).") } t.logger.Debug("streamAppendRow: Row processing completed for row starting with '%s'.", firstCellForLog) return nil } // streamBuildCellContexts creates CellContext objects for a given line in streaming mode. // Parameters: // - position: The section being processed (Header, Row, Footer). // - rowIdx: The row index within its section (always 0 for Header/Footer, row number for Row). // - lineIdx: The line index within the processed lines for this block. // - processedLines: All multi-lines for the current row/header/footer block. // - sectionMerges: Merge states for the section or row (map[int]tw.MergeState). // - sectionConfig: The CellConfig for this section (Header, Row, Footer). // Returns a renderMergeResponse with Current, Previous, Next cells, cellsContent, and the determined Location. func (t *Table) streamBuildCellContexts( position tw.Position, rowIdx, lineIdx int, processedLines [][]string, sectionMerges map[int]tw.MergeState, sectionConfig tw.CellConfig, ) renderMergeResponse { t.logger.Debug("streamBuildCellContexts: Building contexts for position=%s, rowIdx=%d, lineIdx=%d", position, rowIdx, lineIdx) resp := renderMergeResponse{ cells: make(map[int]tw.CellContext), prevCells: nil, nextCells: nil, cellsContent: make([]string, t.streamNumCols), location: tw.LocationMiddle, } if t.streamWidths == nil || t.streamWidths.Len() == 0 || t.streamNumCols == 0 { t.logger.Warn("streamBuildCellContexts: streamWidths is not set or streamNumCols is 0. Returning empty contexts.") return resp } currentLineContent := make([]string, t.streamNumCols) if lineIdx >= 0 && lineIdx < len(processedLines) { currentLineContent = padLine(processedLines[lineIdx], t.streamNumCols) } else { t.logger.Warnf("streamBuildCellContexts: lineIdx %d out of bounds for processedLines (len %d) at position %s, rowIdx %d. Using empty line.", lineIdx, len(processedLines), position, rowIdx) for j := range currentLineContent { currentLineContent[j] = tw.Empty } } resp.cellsContent = currentLineContent colAligns := t.buildAligns(sectionConfig) colPadding := t.buildPadding(sectionConfig.Padding) resp.cells = t.buildCoreCellContexts(currentLineContent, sectionMerges, t.streamWidths, colAligns, colPadding, t.streamNumCols) if t.lastRenderedLineContent != nil && t.lastRenderedPosition.Validate() == nil { resp.prevCells = t.streamRenderedMergeState(t.lastRenderedLineContent, t.lastRenderedMergeState) } totalLinesInBlock := len(processedLines) if lineIdx < totalLinesInBlock-1 { resp.nextCells = make(map[int]tw.CellContext) nextLineContent := padLine(processedLines[lineIdx+1], t.streamNumCols) nextCells := t.buildCoreCellContexts(nextLineContent, sectionMerges, t.streamWidths, colAligns, colPadding, t.streamNumCols) for j := 0; j < t.streamNumCols; j++ { resp.nextCells[j] = nextCells[j] } } isFirstLineOfBlock := (lineIdx == 0) if isFirstLineOfBlock && (t.lastRenderedLineContent == nil || t.lastRenderedPosition != position) { resp.location = tw.LocationFirst } t.logger.Debug("streamBuildCellContexts: Position %s, Row %d, Line %d/%d. Location: %v. Prev Pos: %v. Has Prev: %v.", position, rowIdx, lineIdx, totalLinesInBlock, resp.location, t.lastRenderedPosition, t.lastRenderedLineContent != nil) return resp } // streamCalculateWidths determines the fixed column widths for streaming mode. // It prioritizes widths from StreamConfig.Widths.PerColumn, then StreamConfig.Widths.Global, // then derives from the provided sample data lines. // It populates t.streamWidths and t.streamNumCols if they are currently empty. // The sampleDataLines should be the *raw* input lines (e.g., []string for Header/Footer, or the first row's []string cells for Row). // The paddingConfig should be the CellPadding config relevant to the sample data (Header/Row/Footer). // Returns the determined number of columns. // This function should only be called when t.streamWidths is currently empty. func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) int { if t.streamWidths != nil && t.streamWidths.Len() > 0 { t.logger.Debug("streamCalculateWidths: Called when streaming widths are already set (%d columns). Reusing existing.", t.streamNumCols) return t.streamNumCols } t.logger.Debug("streamCalculateWidths: Calculating streaming widths. Sample data cells: %d. Using section config: %+v", len(sampling), config.Formatting) determinedNumCols := 0 if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 { maxColIdx := -1 t.config.Widths.PerColumn.Each(func(col int, width int) { if col > maxColIdx { maxColIdx = col } }) determinedNumCols = maxColIdx + 1 t.logger.Debug("streamCalculateWidths: Determined numCols (%d) from StreamConfig.Widths.PerColumn", determinedNumCols) } else if len(sampling) > 0 { determinedNumCols = len(sampling) t.logger.Debug("streamCalculateWidths: Determined numCols (%d) from sample data length", determinedNumCols) } else { t.logger.Debug("streamCalculateWidths: Cannot determine numCols (no PerColumn config, no sample data)") t.streamNumCols = 0 t.streamWidths = tw.NewMapper[int, int]() return 0 } t.streamNumCols = determinedNumCols t.streamWidths = tw.NewMapper[int, int]() // Use padding and autowrap from the provided config paddingForWidthCalc := config.Padding autoWrapForWidthCalc := config.Formatting.AutoWrap if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 { t.logger.Debug("streamCalculateWidths: Using widths from StreamConfig.Widths.PerColumn") for i := 0; i < t.streamNumCols; i++ { width, ok := t.config.Widths.PerColumn.OK(i) if !ok { width = 0 } if width > 0 && width < 1 { width = 1 } else if width < 0 { width = 0 } t.streamWidths.Set(i, width) } } else { // No PerColumn config, derive from sampling intelligently t.logger.Debug("streamCalculateWidths: Intelligently deriving widths from sample data content and padding.") tempRequiredWidths := tw.NewMapper[int, int]() // Widths from updateWidths (content + padding) if len(sampling) > 0 { // updateWidths calculates: DisplayWidth(content) + padLeft + padRight t.updateWidths(sampling, tempRequiredWidths, paddingForWidthCalc) } ellipsisWidthBuffer := 0 if autoWrapForWidthCalc == tw.WrapTruncate { ellipsisWidthBuffer = twwidth.Width(tw.CharEllipsis) } varianceBuffer := 2 // Your suggested variance minTotalColWidth := tw.MinimumColumnWidth // Example: if t.config.Stream.MinAutoColumnWidth > 0 { minTotalColWidth = t.config.Stream.MinAutoColumnWidth } for i := 0; i < t.streamNumCols; i++ { // baseCellWidth (content_width + padding_width) comes from tempRequiredWidths.Get(i) // We need to deconstruct it to apply logic to content_width first. sampleContent := "" if i < len(sampling) { sampleContent = t.Trimmer(sampling[i]) } sampleContentDisplayWidth := twwidth.Width(sampleContent) colPad := paddingForWidthCalc.Global if i < len(paddingForWidthCalc.PerColumn) && paddingForWidthCalc.PerColumn[i].Paddable() { colPad = paddingForWidthCalc.PerColumn[i] } currentPadLWidth := twwidth.Width(colPad.Left) currentPadRWidth := twwidth.Width(colPad.Right) currentTotalPaddingWidth := currentPadLWidth + currentPadRWidth // Start with the target content width logic targetContentWidth := sampleContentDisplayWidth if autoWrapForWidthCalc == tw.WrapTruncate { // If content is short, ensure it's at least wide enough for an ellipsis if targetContentWidth < ellipsisWidthBuffer { targetContentWidth = ellipsisWidthBuffer } } targetContentWidth += varianceBuffer // Add variance // Now calculate the total cell width based on this buffered content target + padding calculatedWidth := targetContentWidth + currentTotalPaddingWidth // Apply an absolute minimum total column width if calculatedWidth > 0 && calculatedWidth < minTotalColWidth { t.logger.Debug("streamCalculateWidths: Col %d, InitialCalcW=%d (ContentTarget=%d + Pad=%d) is less than MinTotalW=%d. Adjusting to MinTotalW.", i, calculatedWidth, targetContentWidth, currentTotalPaddingWidth, minTotalColWidth) calculatedWidth = minTotalColWidth } else if calculatedWidth <= 0 && sampleContentDisplayWidth > 0 { // If content exists but calc width is 0 (e.g. large negative variance) // Ensure at least min width or content + padding + buffers fallbackWidth := sampleContentDisplayWidth + currentTotalPaddingWidth if autoWrapForWidthCalc == tw.WrapTruncate { fallbackWidth += ellipsisWidthBuffer } fallbackWidth += varianceBuffer calculatedWidth = tw.Max(minTotalColWidth, fallbackWidth) if calculatedWidth <= 0 && (currentTotalPaddingWidth+1) > 0 { // last resort if all else is zero calculatedWidth = currentTotalPaddingWidth + 1 } else if calculatedWidth <= 0 { calculatedWidth = 1 // absolute last resort } t.logger.Debug("streamCalculateWidths: Col %d, CalculatedW was <=0 despite content. Adjusted to %d.", i, calculatedWidth) } else if calculatedWidth <= 0 && sampleContentDisplayWidth == 0 { // Column is truly empty in sample and buffers didn't make it positive, or minTotalColWidth is 0. // Keep width 0 (it will be hidden by renderer if all content is empty for this col) // Or, if we want empty columns to have a minimum presence (even if just padding): // calculatedWidth = currentTotalPaddingWidth // This would make it just wide enough for padding // For now, let truly empty sample + no min width result in 0. calculatedWidth = 0 // Explicitly set to 0 if it ended up non-positive and no content } t.streamWidths.Set(i, calculatedWidth) t.logger.Debug("streamCalculateWidths: Col %d, SampleContentW=%d, PadW=%d, EllipsisBufIfTruncate=%d, VarianceBuf=%d -> FinalTotalColW=%d", i, sampleContentDisplayWidth, currentTotalPaddingWidth, ellipsisWidthBuffer, varianceBuffer, calculatedWidth) } } // Apply Global Constraint (if t.config.Stream.Widths.Global > 0) if t.config.Widths.Global > 0 && t.streamNumCols > 0 { t.logger.Debug("streamCalculateWidths: Applying global stream width constraint %d", t.config.Widths.Global) currentTotalColumnWidthsSum := 0 t.streamWidths.Each(func(_ int, w int) { currentTotalColumnWidthsSum += w }) separatorWidth := 0 if t.renderer != nil { rendererConfig := t.renderer.Config() if rendererConfig.Settings.Separators.BetweenColumns.Enabled() { separatorWidth = twwidth.Width(rendererConfig.Symbols.Column()) } } else { separatorWidth = 1 // Default if renderer not available yet } totalWidthIncludingSeparators := currentTotalColumnWidthsSum if t.streamNumCols > 1 { totalWidthIncludingSeparators += (t.streamNumCols - 1) * separatorWidth } if t.config.Widths.Global < totalWidthIncludingSeparators && totalWidthIncludingSeparators > 0 { // Added check for total > 0 t.logger.Debug("streamCalculateWidths: Total calculated width (%d incl separators) exceeds global stream width (%d). Shrinking.", totalWidthIncludingSeparators, t.config.Widths.Global) // Target sum for column widths only (global limit - total separator width) targetSumForColumnWidths := t.config.Widths.Global if t.streamNumCols > 1 { targetSumForColumnWidths -= (t.streamNumCols - 1) * separatorWidth } if targetSumForColumnWidths < t.streamNumCols && t.streamNumCols > 0 { // Ensure at least 1 per column if possible targetSumForColumnWidths = t.streamNumCols } else if targetSumForColumnWidths < 0 { targetSumForColumnWidths = 0 } scaleFactor := float64(targetSumForColumnWidths) / float64(currentTotalColumnWidthsSum) if currentTotalColumnWidthsSum <= 0 { scaleFactor = 0 } // Avoid division by zero or negative scale adjustedSum := 0 for i := 0; i < t.streamNumCols; i++ { originalColWidth := t.streamWidths.Get(i) if originalColWidth == 0 { continue } // Don't scale hidden columns scaledWidth := 0 if scaleFactor > 0 { scaledWidth = int(math.Round(float64(originalColWidth) * scaleFactor)) } if scaledWidth < 1 && originalColWidth > 0 { // Ensure at least 1 if original had width and scaling made it too small scaledWidth = 1 } else if scaledWidth < 0 { // Should not happen with math.Round on positive*positive scaledWidth = 0 } t.streamWidths.Set(i, scaledWidth) adjustedSum += scaledWidth } // Distribute rounding errors to meet targetSumForColumnWidths remainingSpace := targetSumForColumnWidths - adjustedSum t.logger.Debug("streamCalculateWidths: Scaling complete. TargetSum=%d, AchievedSum=%d, RemSpace=%d", targetSumForColumnWidths, adjustedSum, remainingSpace) // Distribute remainingSpace (positive or negative) among non-zero width columns if remainingSpace != 0 && t.streamNumCols > 0 { colsToAdjust := []int{} t.streamWidths.Each(func(col int, w int) { if w > 0 { // Only consider columns that currently have width colsToAdjust = append(colsToAdjust, col) } }) if len(colsToAdjust) > 0 { for i := 0; i < int(math.Abs(float64(remainingSpace))); i++ { colIdx := colsToAdjust[i%len(colsToAdjust)] currentColWidth := t.streamWidths.Get(colIdx) if remainingSpace > 0 { t.streamWidths.Set(colIdx, currentColWidth+1) } else if remainingSpace < 0 && currentColWidth > 1 { // Don't reduce below 1 t.streamWidths.Set(colIdx, currentColWidth-1) } } } } t.logger.Debug("streamCalculateWidths: Widths after scaling and distribution: %v", t.streamWidths) } else { t.logger.Debug("streamCalculateWidths: Total calculated width (%d) fits global stream width (%d). No scaling needed.", totalWidthIncludingSeparators, t.config.Widths.Global) } } // Final sanitization t.streamWidths.Each(func(col int, width int) { if width < 0 { t.streamWidths.Set(col, 0) } }) t.logger.Debug("streamCalculateWidths: Final derived stream widths after all adjustments (%d columns): %v", t.streamNumCols, t.streamWidths) return t.streamNumCols } // streamRenderBottomBorder renders the bottom border of the table in streaming mode. // It uses the fixed streamWidths and the last rendered content to create the border context. // It assumes Start() has been called and t.hasPrinted is true. // Returns an error if rendering fails. func (t *Table) streamRenderBottomBorder() error { if t.streamWidths == nil || t.streamWidths.Len() == 0 { t.logger.Debug("streamRenderBottomBorder: No stream widths available, skipping bottom border.") return nil } cfg := t.renderer.Config() if !cfg.Borders.Bottom.Enabled() || !cfg.Settings.Lines.ShowBottom.Enabled() { t.logger.Debug("streamRenderBottomBorder: Bottom border disabled in config, skipping.") return nil } // The bottom border's "Current" context is the last rendered content line currentCells := make(map[int]tw.CellContext) if t.lastRenderedLineContent != nil { // Use a helper to convert last rendered state to cell contexts currentCells = t.streamRenderedMergeState(t.lastRenderedLineContent, t.lastRenderedMergeState) } else { // No content was ever rendered, but we might still want a bottom border if a top border was drawn. // Create empty cell contexts. for i := 0; i < t.streamNumCols; i++ { currentCells[i] = tw.CellContext{Width: t.streamWidths.Get(i)} } t.logger.Debug("streamRenderBottomBorder: No previous content line, creating empty context for bottom border.") } f := t.renderer f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Current: currentCells, // Context of the line *above* the bottom border Previous: nil, // No line before this, relative to the border itself (or use lastRendered's previous?) Next: nil, // No line after the bottom border Position: t.lastRenderedPosition, // Position of the content above the border (Row or Footer) Location: tw.LocationEnd, // This is the absolute end }, Level: tw.LevelFooter, // Bottom border is LevelFooter IsSubRow: false, NormalizedWidths: t.streamWidths, }) t.logger.Debug("streamRenderBottomBorder: Bottom border rendered.") return nil } // streamRenderFooter renders the stored footer lines in streaming mode. // It's called by Close(). It renders the Row/Footer separator line first. // It assumes Start() has been called and t.hasPrinted is true. // Returns an error if rendering fails. func (t *Table) streamRenderFooter(processedFooterLines [][]string) error { t.logger.Debug("streamRenderFooter: Rendering %d processed footer lines.", len(processedFooterLines)) if t.streamWidths == nil || t.streamWidths.Len() == 0 || t.streamNumCols == 0 { t.logger.Warn("streamRenderFooter: No stream widths or columns defined. Cannot render footer.") return errors.New("cannot render stream footer without defined column widths") } if len(processedFooterLines) == 0 { t.logger.Debug("streamRenderFooter: No footer lines to render.") return nil } f := t.renderer cfg := t.renderer.Config() // Render Row/Footer or Header/Footer Separator Line // This separator is drawn if ShowFooterLine is enabled AND there was content before the footer. // The last rendered position (t.lastRenderedPosition) should be Row or Header or "separator". if (t.lastRenderedPosition == tw.Row || t.lastRenderedPosition == tw.Header || t.lastRenderedPosition == tw.Position("separator")) && cfg.Settings.Lines.ShowFooterLine.Enabled() { t.logger.Debug("streamRenderFooter: Rendering Row/Footer or Header/Footer separator line.") // Previous context is the last line rendered before this footer prevCells := t.streamRenderedMergeState(t.lastRenderedLineContent, t.lastRenderedMergeState) // Next context is the first line of this footer var nextCells map[int]tw.CellContext = nil if len(processedFooterLines) > 0 { // Need merge states for the footer section. // Since footer is processed once and stored, detect merges on its raw input once. // This requires access to the *original* raw footer strings passed to Footer(). // For simplicity now, assume no complex horizontal merges in footer for this separator line context. // A better approach: streamStoreFooter should also calculate and store footerMerges. // For now, create nextCells without specific merge info for the separator line. // Or, call prepareWithMerges on the *stored processed* lines, which might be okay for simple cases. // Let's pass nil for sectionMerges to streamBuildCellContexts for this specific Next context. // It will result in default (no-merge) states. // For now, let's build nextCells manually for the separator line context nextCells = make(map[int]tw.CellContext) firstFooterLineContent := padLine(processedFooterLines[0], t.streamNumCols) // Footer merges should be calculated in streamStoreFooter and stored if needed. // For now, assume no merges for this 'Next' context. for j := 0; j < t.streamNumCols; j++ { nextCells[j] = tw.CellContext{Data: firstFooterLineContent[j], Width: t.streamWidths.Get(j)} } } separatorLevel := tw.LevelFooter // Line before footer section is LevelFooter separatorPosition := tw.Footer // Positioned relative to the footer it precedes separatorLocation := tw.LocationMiddle f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Current: prevCells, // Context of line above separator Previous: nil, // No line before Current in this specific context Next: nextCells, // Context of line below separator (first footer line) Position: separatorPosition, Location: separatorLocation, }, Level: separatorLevel, IsSubRow: false, NormalizedWidths: t.streamWidths, }) t.lastRenderedPosition = tw.Position("separator") // Update state t.lastRenderedLineContent = nil t.lastRenderedMergeState = nil t.logger.Debug("streamRenderFooter: Footer separator line rendered.") } // End Render Separator Line // Detect horizontal merges for the footer section based on its (assumed stored) raw input. // This is tricky because streamStoreFooter gets []string, but prepareWithMerges expects [][]string. // For simplicity, if complex merges are needed in footer, streamStoreFooter should // have received raw data, called prepareWithMerges, and stored those merges. // For now, assume no complex horizontal merges in footer or pass nil for sectionMerges. // Let's assume footerMerges were calculated and stored as `t.streamFooterMerges map[int]tw.MergeState` // by `streamStoreFooter`. For this example, we'll pass nil, meaning no merges. var footerMerges map[int]tw.MergeState = nil // Placeholder totalFooterLines := len(processedFooterLines) for i := 0; i < totalFooterLines; i++ { resp := t.streamBuildCellContexts( tw.Footer, 0, // Row index within Footer (always 0) i, // Line index processedFooterLines, footerMerges, // Pass footer-specific merges if calculated and stored t.config.Footer, ) // Special Location logic for the *very last line* of the table if this footer line is it. // This is complex because bottom border might follow. // Let streamBuildCellContexts handle LocationFirst/Middle for now. // streamRenderBottomBorder will handle the final LocationEnd for its line. // If this footer line is the last content and no bottom border, *it* should be LocationEnd. // If this is the last line of the last content block (footer), and no bottom border will be drawn, // its Location should be End. isLastLineOfTableContent := (i == totalFooterLines-1) && !(cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled()) if isLastLineOfTableContent { resp.location = tw.LocationEnd t.logger.Debug("streamRenderFooter: Setting LocationEnd for last footer line as no bottom border will follow.") } f.Footer([][]string{resp.cellsContent}, tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Current: resp.cells, Previous: resp.prevCells, Next: resp.nextCells, // Next is nil if last line of footer block Position: tw.Footer, Location: resp.location, }, Level: tw.LevelFooter, IsSubRow: (i > 0), NormalizedWidths: t.streamWidths, }) t.lastRenderedLineContent = resp.cellsContent t.lastRenderedMergeState = make(map[int]tw.MergeState) for colIdx, cellCtx := range resp.cells { t.lastRenderedMergeState[colIdx] = cellCtx.Merge } t.lastRenderedPosition = tw.Footer } t.logger.Debug("streamRenderFooter: Footer content rendering completed.") return nil } // streamRenderHeader processes and renders the header section in streaming mode. // It calculates/uses fixed stream widths, processes content, renders borders/lines, // and updates streaming state. // It assumes Start() has already been called and t.hasPrinted is true. func (t *Table) streamRenderHeader(headers []string) error { t.logger.Debug("streamRenderHeader called with headers: %v", headers) if !t.config.Stream.Enable { return errors.New("streaming mode is disabled") } if t.headerRendered { t.logger.Warn("streamRenderHeader called but header already rendered. Ignoring.") return nil } if err := t.ensureStreamWidthsCalculated(headers, t.config.Header); err != nil { return err } _, headerMerges, _ := t.prepareWithMerges([][]string{headers}, t.config.Header, tw.Header) processedHeaderLines := t.prepareContent(headers, t.config.Header) t.logger.Debug("streamRenderHeader: Processed header lines: %d", len(processedHeaderLines)) if t.streamNumCols > 0 { t.headerRendered = true } if len(processedHeaderLines) == 0 && t.streamNumCols == 0 { t.logger.Debug("streamRenderHeader: No header content and no columns determined.") return nil } f := t.renderer cfg := t.renderer.Config() if t.lastRenderedPosition == "" && cfg.Borders.Top.Enabled() && cfg.Settings.Lines.ShowTop.Enabled() { t.logger.Debug("streamRenderHeader: Rendering table top border.") var nextCellsCtx map[int]tw.CellContext if len(processedHeaderLines) > 0 { firstHeaderLineResp := t.streamBuildCellContexts( tw.Header, 0, 0, processedHeaderLines, headerMerges, t.config.Header, ) nextCellsCtx = firstHeaderLineResp.cells } f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Next: nextCellsCtx, Position: tw.Header, Location: tw.LocationFirst, }, Level: tw.LevelHeader, IsSubRow: false, NormalizedWidths: t.streamWidths, }) t.logger.Debug("streamRenderHeader: Top border rendered.") } hasTopPadding := t.config.Header.Padding.Global.Top != tw.Empty if hasTopPadding { resp := t.streamBuildCellContexts(tw.Header, 0, -1, nil, headerMerges, t.config.Header) resp.cellsContent = t.buildPaddingLineContents(t.config.Header.Padding.Global.Top, t.streamWidths, t.streamNumCols, headerMerges) resp.location = tw.LocationFirst t.logger.Debug("streamRenderHeader: Rendering header top padding line: %v (loc: %v)", resp.cellsContent, resp.location) f.Header([][]string{resp.cellsContent}, tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Current: resp.cells, Previous: resp.prevCells, Next: resp.nextCells, Position: tw.Header, Location: resp.location, }, Level: tw.LevelHeader, IsSubRow: true, NormalizedWidths: t.streamWidths, }) t.lastRenderedLineContent = resp.cellsContent t.lastRenderedMergeState = make(map[int]tw.MergeState) for colIdx, cellCtx := range resp.cells { t.lastRenderedMergeState[colIdx] = cellCtx.Merge } t.lastRenderedPosition = tw.Header } totalHeaderLines := len(processedHeaderLines) for i := 0; i < totalHeaderLines; i++ { resp := t.streamBuildCellContexts(tw.Header, 0, i, processedHeaderLines, headerMerges, t.config.Header) t.logger.Debug("streamRenderHeader: Rendering header content line %d/%d with location %v", i, totalHeaderLines, resp.location) f.Header([][]string{resp.cellsContent}, tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Current: resp.cells, Previous: resp.prevCells, Next: resp.nextCells, Position: tw.Header, Location: resp.location, }, Level: tw.LevelHeader, IsSubRow: i > 0, NormalizedWidths: t.streamWidths, }) t.lastRenderedLineContent = resp.cellsContent t.lastRenderedMergeState = make(map[int]tw.MergeState) for colIdx, cellCtx := range resp.cells { t.lastRenderedMergeState[colIdx] = cellCtx.Merge } t.lastRenderedPosition = tw.Header } hasBottomPadding := t.config.Header.Padding.Global.Bottom != tw.Empty if hasBottomPadding { resp := t.streamBuildCellContexts(tw.Header, 0, totalHeaderLines, nil, headerMerges, t.config.Header) resp.cellsContent = t.buildPaddingLineContents(t.config.Header.Padding.Global.Bottom, t.streamWidths, t.streamNumCols, headerMerges) resp.location = tw.LocationEnd t.logger.Debug("streamRenderHeader: Rendering header bottom padding line: %v (loc: %v)", resp.cellsContent, resp.location) f.Header([][]string{resp.cellsContent}, tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Current: resp.cells, Previous: resp.prevCells, Next: resp.nextCells, Position: tw.Header, Location: resp.location, }, Level: tw.LevelHeader, IsSubRow: true, NormalizedWidths: t.streamWidths, }) t.lastRenderedLineContent = resp.cellsContent t.lastRenderedMergeState = make(map[int]tw.MergeState) for colIdx, cellCtx := range resp.cells { t.lastRenderedMergeState[colIdx] = cellCtx.Merge } t.lastRenderedPosition = tw.Header } if cfg.Settings.Lines.ShowHeaderLine.Enabled() && (t.firstRowRendered || len(t.streamFooterLines) > 0) { t.logger.Debug("streamRenderHeader: Rendering header separator line.") resp := t.streamBuildCellContexts(tw.Header, 0, totalHeaderLines-1, processedHeaderLines, headerMerges, t.config.Header) f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: t.streamWidths, ColMaxWidths: tw.CellWidth{PerColumn: t.streamWidths}, Current: resp.cells, Previous: resp.prevCells, Next: nil, Position: tw.Header, Location: tw.LocationMiddle, }, Level: tw.LevelBody, IsSubRow: false, NormalizedWidths: t.streamWidths, }) t.lastRenderedPosition = tw.Position("separator") t.lastRenderedLineContent = nil t.lastRenderedMergeState = nil } t.logger.Debug("streamRenderHeader: Header content rendering completed.") return nil } // streamRenderedMergeState converts the stored last rendered line content // and its merge states into a map of CellContext, suitable for providing // context (e.g., "Current" or "Previous") to the renderer. // It uses the fixed streamWidths. func (t *Table) streamRenderedMergeState( lineContent []string, lineMergeStates map[int]tw.MergeState, ) map[int]tw.CellContext { cells := make(map[int]tw.CellContext) if t.streamWidths == nil || t.streamWidths.Len() == 0 || t.streamNumCols == 0 { t.logger.Warn("streamRenderedMergeState: streamWidths not set or streamNumCols is 0. Returning empty cell contexts.") return cells } // Ensure lineContent is padded to streamNumCols if it's not nil var paddedLineContent []string if lineContent != nil { paddedLineContent = padLine(lineContent, t.streamNumCols) } else { // If lineContent is nil (e.g. after a separator), create an empty padded line paddedLineContent = make([]string, t.streamNumCols) for i := range paddedLineContent { paddedLineContent[i] = tw.Empty } } for j := 0; j < t.streamNumCols; j++ { cellData := paddedLineContent[j] colWidth := t.streamWidths.Get(j) mergeState := tw.MergeState{} // Default to no merge if lineMergeStates != nil { if state, ok := lineMergeStates[j]; ok { mergeState = state } } // For context purposes (like Previous or Current for a border line), // Align and Padding are often less critical than Data, Width, and Merge. // We can use default/empty Align and Padding here. cells[j] = tw.CellContext{ Data: cellData, Align: tw.AlignDefault, // Or tw.AlignNone if preferred for context-only cells Padding: tw.Padding{}, // Empty padding Width: colWidth, Merge: mergeState, } } return cells } // streamStoreFooter processes the footer content and stores it for later rendering by Close() // in streaming mode. It ensures stream widths are calculated if not already set. func (t *Table) streamStoreFooter(footers []string) error { t.logger.Debug("streamStoreFooter called with footers: %v", footers) if !t.config.Stream.Enable { return errors.New("streaming mode is disabled") } if len(footers) == 0 { t.logger.Debug("streamStoreFooter: Empty footer cells, storing empty footer lines.") t.streamFooterLines = [][]string{} return nil } if err := t.ensureStreamWidthsCalculated(footers, t.config.Footer); err != nil { t.logger.Warnf("streamStoreFooter: Failed to determine column count from footer data: %v", err) t.streamFooterLines = [][]string{} return nil } if t.streamNumCols > 0 && len(footers) != t.streamNumCols { t.logger.Warnf("streamStoreFooter: Input footer column count (%d) does not match fixed stream column count (%d). Padding/Truncating input footers.", len(footers), t.streamNumCols) if len(footers) < t.streamNumCols { paddedFooters := make([]string, t.streamNumCols) copy(paddedFooters, footers) for i := len(footers); i < t.streamNumCols; i++ { paddedFooters[i] = tw.Empty } footers = paddedFooters } else { footers = footers[:t.streamNumCols] } } if t.streamNumCols == 0 { t.logger.Warn("streamStoreFooter: streamNumCols is 0, cannot process/store footer lines meaningfully.") t.streamFooterLines = [][]string{} return nil } t.streamFooterLines = t.prepareContent(footers, t.config.Footer) t.logger.Debug("streamStoreFooter: Processed and stored footer lines: %d lines. Content: %v", len(t.streamFooterLines), t.streamFooterLines) return nil } tablewriter-1.0.9/tablewriter.go000066400000000000000000002436621504170215200167320ustar00rootroot00000000000000package tablewriter import ( "bytes" "github.com/olekukonko/errors" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/tablewriter/pkg/twwarp" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "io" "math" "os" "reflect" "runtime" "strings" "sync" ) // Table represents a table instance with content and rendering capabilities. type Table struct { writer io.Writer // Destination for table output rows [][][]string // Row data, supporting multi-line cells headers [][]string // Header content footers [][]string // Footer content headerWidths tw.Mapper[int, int] // Computed widths for header columns rowWidths tw.Mapper[int, int] // Computed widths for row columns footerWidths tw.Mapper[int, int] // Computed widths for footer columns renderer tw.Renderer // Engine for rendering the table config Config // Table configuration settings stringer any // Function to convert rows to strings newLine string // Newline character (e.g., "\n") hasPrinted bool // Indicates if the table has been rendered logger *ll.Logger // Debug trace log trace *bytes.Buffer // Debug trace log // Caption fields caption tw.Caption // streaming streamWidths tw.Mapper[int, int] // Fixed column widths for streaming mode, calculated once streamFooterLines [][]string // Processed footer lines for streaming, stored until Close(). headerRendered bool // Tracks if header has been rendered in streaming mode firstRowRendered bool // Tracks if the first data row has been rendered in streaming mode lastRenderedLineContent []string // Content of the very last line rendered (for Previous context in streaming) lastRenderedMergeState tw.Mapper[int, tw.MergeState] // Merge state of the very last line rendered (for Previous context in streaming) lastRenderedPosition tw.Position // Position (Header/Row/Footer/Separator) of the last line rendered (for Previous context in streaming) streamNumCols int // The derived number of columns in streaming mode streamRowCounter int // Counter for rows rendered in streaming mode (0-indexed logical rows) // cache stringerCache map[reflect.Type]reflect.Value // Cache for stringer reflection stringerCacheMu sync.RWMutex // Mutex for thread-safe cache access stringerCacheEnabled bool // Flag to enable/disable caching batchRenderNumCols int isBatchRenderNumColsSet bool } // renderContext holds the core state for rendering the table. type renderContext struct { table *Table // Reference to the table instance renderer tw.Renderer // Renderer instance cfg tw.Rendition // Renderer configuration numCols int // Total number of columns headerLines [][]string // Processed header lines rowLines [][][]string // Processed row lines footerLines [][]string // Processed footer lines widths tw.Mapper[tw.Position, tw.Mapper[int, int]] // Widths per section footerPrepared bool // Tracks if footer is prepared emptyColumns []bool // Tracks which original columns are empty (true if empty) visibleColCount int // Count of columns that are NOT empty logger *ll.Logger // Debug trace log } // mergeContext holds state related to cell merging. type mergeContext struct { headerMerges map[int]tw.MergeState // Merge states for header columns rowMerges []map[int]tw.MergeState // Merge states for each row footerMerges map[int]tw.MergeState // Merge states for footer columns horzMerges map[tw.Position]map[int]bool // Tracks horizontal merges (unused) } // helperContext holds additional data for rendering helpers. type helperContext struct { position tw.Position // Section being processed (Header, Row, Footer) rowIdx int // Row index within section lineIdx int // Line index within row location tw.Location // Boundary location (First, Middle, End) line []string // Current line content } // renderMergeResponse holds cell context data from rendering operations. type renderMergeResponse struct { cells map[int]tw.CellContext // Current line cells prevCells map[int]tw.CellContext // Previous line cells nextCells map[int]tw.CellContext // Next line cells location tw.Location // Determined Location for this line cellsContent []string } // NewTable creates a new table instance with specified writer and options. // Parameters include writer for output and optional configuration options. // Returns a pointer to the initialized Table instance. func NewTable(w io.Writer, opts ...Option) *Table { t := &Table{ writer: w, headerWidths: tw.NewMapper[int, int](), rowWidths: tw.NewMapper[int, int](), footerWidths: tw.NewMapper[int, int](), renderer: renderer.NewBlueprint(), config: defaultConfig(), newLine: tw.NewLine, trace: &bytes.Buffer{}, // Streaming streamWidths: tw.NewMapper[int, int](), // Initialize empty mapper for streaming widths lastRenderedMergeState: tw.NewMapper[int, tw.MergeState](), headerRendered: false, firstRowRendered: false, lastRenderedPosition: "", streamNumCols: 0, streamRowCounter: 0, // Cache stringerCache: make(map[reflect.Type]reflect.Value), stringerCacheEnabled: false, // Disabled by default } // set Options t.Options(opts...) t.logger.Infof("Table initialized with %d options", len(opts)) return t } // NewWriter creates a new table with default settings for backward compatibility. // It logs the creation if debugging is enabled. func NewWriter(w io.Writer) *Table { t := NewTable(w) if t.logger != nil { t.logger.Debug("NewWriter created buffered Table") } return t } // Caption sets the table caption (legacy method). // Defaults to BottomCenter alignment, wrapping to table width. // Use SetCaptionOptions for more control. func (t *Table) Caption(caption tw.Caption) *Table { // This is the one we modified originalSpot := caption.Spot originalAlign := caption.Align if caption.Spot == tw.SpotNone { caption.Spot = tw.SpotBottomCenter t.logger.Debugf("[Table.Caption] Input Spot was SpotNone, defaulting Spot to SpotBottomCenter (%d)", caption.Spot) } if caption.Align == "" || caption.Align == tw.AlignDefault || caption.Align == tw.AlignNone { switch caption.Spot { case tw.SpotTopLeft, tw.SpotBottomLeft: caption.Align = tw.AlignLeft case tw.SpotTopRight, tw.SpotBottomRight: caption.Align = tw.AlignRight default: caption.Align = tw.AlignCenter } t.logger.Debugf("[Table.Caption] Input Align was empty/default, defaulting Align to %s for Spot %d", caption.Align, caption.Spot) } t.caption = caption // t.caption on the struct is now updated. t.logger.Debugf("Caption method called: Input(Spot:%v, Align:%q), Final(Spot:%v, Align:%q), Text:'%.20s', MaxWidth:%d", originalSpot, originalAlign, t.caption.Spot, t.caption.Align, t.caption.Text, t.caption.Width) return t } // Append adds data to the current row being built for the table. // This method always contributes to a single logical row in the table. // To add multiple distinct rows, call Append multiple times (once for each row's data) // or use the Bulk() method if providing a slice where each element is a row. func (t *Table) Append(rows ...interface{}) error { t.ensureInitialized() if t.config.Stream.Enable && t.hasPrinted { // Streaming logic remains unchanged, as AutoHeader is a batch-mode concept. t.logger.Debugf("Append() called in streaming mode with %d items for a single row", len(rows)) var rowItemForStream interface{} if len(rows) == 1 { rowItemForStream = rows[0] } else { rowItemForStream = rows } if err := t.streamAppendRow(rowItemForStream); err != nil { t.logger.Errorf("Error rendering streaming row: %v", err) return errors.Newf("failed to stream append row").Wrap(err) } return nil } // Batch Mode Logic t.logger.Debugf("Append (Batch) received %d arguments: %v", len(rows), rows) var cellsSource interface{} if len(rows) == 1 { cellsSource = rows[0] } else { cellsSource = rows } // Check if we should attempt to auto-generate headers from this append operation. // Conditions: AutoHeader is on, no headers are set yet, and this is the first data row. isFirstRow := len(t.rows) == 0 if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 && isFirstRow { t.logger.Debug("Append: Triggering AutoHeader for the first row.") headers := t.extractHeadersFromStruct(cellsSource) if len(headers) > 0 { // Set the extracted headers. The Header() method handles the rest. t.Header(headers) } } // The rest of the function proceeds as before, converting the data to string lines. lines, err := t.toStringLines(cellsSource, t.config.Row) if err != nil { t.logger.Errorf("Append (Batch) failed for cellsSource %v: %v", cellsSource, err) return err } t.rows = append(t.rows, lines) t.logger.Debugf("Append (Batch) completed for one row, total rows in table: %d", len(t.rows)) return nil } // Bulk adds multiple rows from a slice to the table. // If Behavior.AutoHeader is enabled, no headers set, and rows is a slice of structs, // automatically extracts/sets headers from the first struct. func (t *Table) Bulk(rows interface{}) error { rv := reflect.ValueOf(rows) if rv.Kind() != reflect.Slice { return errors.Newf("Bulk expects a slice, got %T", rows) } if rv.Len() == 0 { return nil } // AutoHeader logic remains here, as it's a "Bulk" operation concept. if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 { first := rv.Index(0).Interface() // We can now correctly get headers from pointers or embedded structs headers := t.extractHeadersFromStruct(first) if len(headers) > 0 { t.Header(headers) } } // The rest of the logic is now just a loop over Append. for i := 0; i < rv.Len(); i++ { row := rv.Index(i).Interface() if err := t.Append(row); err != nil { // Use Append return err } } return nil } // Config returns the current table configuration. // No parameters are required. // Returns the Config struct with current settings. func (t *Table) Config() Config { return t.config } // Configure updates the table's configuration using a provided function. // Parameter fn is a function that modifies the Config struct. // Returns the Table instance for method chaining. func (t *Table) Configure(fn func(cfg *Config)) *Table { fn(&t.config) // Let the user modify the config directly // Handle any immediate side-effects of config changes, e.g., logger state if t.config.Debug { t.logger.Enable() t.logger.Resume() // in case it was suspended } else { t.logger.Disable() t.logger.Suspend() // suspend totally, especially because of tight loops } t.logger.Debugf("Configure complete. New t.config: %+v", t.config) return t } // Debug retrieves the accumulated debug trace logs. // No parameters are required. // Returns a slice of debug messages including renderer logs. func (t *Table) Debug() *bytes.Buffer { return t.trace } // Header sets the table's header content, padding to match column count. // Parameter elements is a slice of strings for header content. // No return value. // In streaming mode, this processes and renders the header immediately. func (t *Table) Header(elements ...any) { t.ensureInitialized() t.logger.Debugf("Header() method called with raw variadic elements: %v (len %d). Streaming: %v, Started: %v", elements, len(elements), t.config.Stream.Enable, t.hasPrinted) // just forget if t.config.Behavior.Header.Hide.Enabled() { return } // add come common default if t.config.Header.Formatting.AutoFormat == tw.Unknown { t.config.Header.Formatting.AutoFormat = tw.On } if t.config.Stream.Enable && t.hasPrinted { // Streaming Path actualCellsToProcess := t.processVariadic(elements) headersAsStrings, err := t.convertCellsToStrings(actualCellsToProcess, t.config.Header) if err != nil { t.logger.Errorf("Header(): Failed to convert header elements to strings for streaming: %v", err) headersAsStrings = []string{} // Use empty on error } errStream := t.streamRenderHeader(headersAsStrings) // streamRenderHeader handles padding to streamNumCols internally if errStream != nil { t.logger.Errorf("Error rendering streaming header: %v", errStream) } return } // Batch Path processedElements := t.processVariadic(elements) t.logger.Debugf("Header() (Batch): Effective cells to process: %v", processedElements) headersAsStrings, err := t.convertCellsToStrings(processedElements, t.config.Header) if err != nil { t.logger.Errorf("Header() (Batch): Failed to convert to strings: %v", err) t.headers = [][]string{} // Set to empty on error return } // prepareContent uses t.config.Header for AutoFormat and MaxWidth constraints. // It processes based on the number of columns in headersAsStrings. preparedHeaderLines := t.prepareContent(headersAsStrings, t.config.Header) t.headers = preparedHeaderLines // Store directly. Padding to t.maxColumns() will happen in prepareContexts. t.logger.Debugf("Header set (batch mode), lines stored: %d. First line if exists: %v", len(t.headers), func() []string { if len(t.headers) > 0 { return t.headers[0] } else { return nil } }()) } // Footer sets the table's footer content, padding to match column count. // Parameter footers is a slice of strings for footer content. // No return value. // Footer sets the table's footer content. // Parameter footers is a slice of strings for footer content. // In streaming mode, this processes and stores the footer for rendering by Close(). func (t *Table) Footer(elements ...any) { t.ensureInitialized() t.logger.Debugf("Footer() method called with raw variadic elements: %v (len %d). Streaming: %v, Started: %v", elements, len(elements), t.config.Stream.Enable, t.hasPrinted) // just forget if t.config.Behavior.Footer.Hide.Enabled() { return } if t.config.Stream.Enable && t.hasPrinted { // Streaming Path actualCellsToProcess := t.processVariadic(elements) footersAsStrings, err := t.convertCellsToStrings(actualCellsToProcess, t.config.Footer) if err != nil { t.logger.Errorf("Footer(): Failed to convert footer elements to strings for streaming: %v", err) footersAsStrings = []string{} // Use empty on error } errStream := t.streamStoreFooter(footersAsStrings) // streamStoreFooter handles padding to streamNumCols internally if errStream != nil { t.logger.Errorf("Error processing streaming footer: %v", errStream) } return } // Batch Path processedElements := t.processVariadic(elements) t.logger.Debugf("Footer() (Batch): Effective cells to process: %v", processedElements) footersAsStrings, err := t.convertCellsToStrings(processedElements, t.config.Footer) if err != nil { t.logger.Errorf("Footer() (Batch): Failed to convert to strings: %v", err) t.footers = [][]string{} // Set to empty on error return } preparedFooterLines := t.prepareContent(footersAsStrings, t.config.Footer) t.footers = preparedFooterLines // Store directly. Padding to t.maxColumns() will happen in prepareContexts. t.logger.Debugf("Footer set (batch mode), lines stored: %d. First line if exists: %v", len(t.footers), func() []string { if len(t.footers) > 0 { return t.footers[0] } else { return nil } }()) } // Options updates the table's Options using a provided function. // Parameter opts is a function that modifies the Table struct. // Returns the Table instance for method chaining. func (t *Table) Options(opts ...Option) *Table { // add logger if t.logger == nil { t.logger = ll.New("table").Handler(lh.NewTextHandler(t.trace)) } // loop through options for _, opt := range opts { opt(t) } // force debugging mode if set // This should be move away form WithDebug if t.config.Debug == true { t.logger.Enable() t.logger.Resume() } else { t.logger.Disable() t.logger.Suspend() } // Get additional system information for debugging goVersion := runtime.Version() goOS := runtime.GOOS goArch := runtime.GOARCH numCPU := runtime.NumCPU() t.logger.Infof("Environment: LC_CTYPE=%s, LANG=%s, TERM=%s", os.Getenv("LC_CTYPE"), os.Getenv("LANG"), os.Getenv("TERM")) t.logger.Infof("Go Runtime: Version=%s, OS=%s, Arch=%s, CPUs=%d", goVersion, goOS, goArch, numCPU) // send logger to renderer // this will overwrite the default logger t.renderer.Logger(t.logger) return t } // Reset clears all data (headers, rows, footers, caption) and rendering state // from the table, allowing the Table instance to be reused for a new table // with the same configuration and writer. // It does NOT reset the configuration itself (set by NewTable options or Configure) // or the underlying io.Writer. func (t *Table) Reset() { t.logger.Debug("Reset() called. Clearing table data and render state.") // Clear data slices t.rows = nil // Or t.rows = make([][][]string, 0) t.headers = nil // Or t.headers = make([][]string, 0) t.footers = nil // Or t.footers = make([][]string, 0) // Reset width mappers (important for recalculating widths for the new table) t.headerWidths = tw.NewMapper[int, int]() t.rowWidths = tw.NewMapper[int, int]() t.footerWidths = tw.NewMapper[int, int]() // Reset caption t.caption = tw.Caption{} // Reset to zero value // Reset rendering state flags t.hasPrinted = false // Critical for allowing Render() or stream Start() again // Reset streaming-specific state // (Important if the table was used in streaming mode and might be reused in batch or another stream) t.streamWidths = tw.NewMapper[int, int]() t.streamFooterLines = nil t.headerRendered = false t.firstRowRendered = false t.lastRenderedLineContent = nil t.lastRenderedMergeState = tw.NewMapper[int, tw.MergeState]() // Re-initialize t.lastRenderedPosition = "" t.streamNumCols = 0 t.streamRowCounter = 0 // The stringer and its cache are part of the table's configuration, if t.stringerCacheEnabled { t.stringerCacheMu.Lock() t.stringerCache = make(map[reflect.Type]reflect.Value) t.stringerCacheMu.Unlock() t.logger.Debug("Reset(): Stringer cache cleared.") } // If the renderer has its own state that needs resetting after a table is done, // this would be the place to call a renderer.Reset() method if it existed. // Most current renderers are stateless per render call or reset in their Start/Close. // For instance, HTML and SVG renderers have their own Reset method. // It might be good practice to call it if available. if r, ok := t.renderer.(interface{ Reset() }); ok { t.logger.Debug("Reset(): Calling Reset() on the current renderer.") r.Reset() } t.logger.Info("Table instance has been reset.") } // Render triggers the table rendering process to the configured writer. // No parameters are required. // Returns an error if rendering fails. func (t *Table) Render() error { return t.render() } // Trimmer trims whitespace from a string based on the Table’s configuration. // It conditionally applies strings.TrimSpace to the input string if the TrimSpace behavior // is enabled in t.config.Behavior, otherwise returning the string unchanged. This method // is used in the logging library to format strings for tabular output, ensuring consistent // display in log messages. Thread-safe as it only reads configuration and operates on the // input string. func (t *Table) Trimmer(str string) string { if t.config.Behavior.TrimSpace.Enabled() { return strings.TrimSpace(str) } return str } // appendSingle adds a single row to the table's row data. // Parameter row is the data to append, converted via stringer if needed. // Returns an error if conversion or appending fails. func (t *Table) appendSingle(row interface{}) error { t.ensureInitialized() // Already here if t.config.Stream.Enable && t.hasPrinted { // If streaming is active t.logger.Debugf("appendSingle: Dispatching to streamAppendRow for row: %v", row) return t.streamAppendRow(row) // Call the streaming render function } // Existing batch logic: t.logger.Debugf("appendSingle: Processing for batch mode, row: %v", row) // toStringLines now uses the new convertCellsToStrings internally, then prepareContent. // This is fine for batch. lines, err := t.toStringLines(row, t.config.Row) if err != nil { t.logger.Debugf("Error in toStringLines (batch mode): %v", err) return err } t.rows = append(t.rows, lines) // Add to batch storage t.logger.Debugf("Row appended to batch t.rows, total batch rows: %d", len(t.rows)) return nil } // buildAligns constructs a map of column alignments from configuration. // Parameter config provides alignment settings for the section. // Returns a map of column indices to alignment settings. func (t *Table) buildAligns(config tw.CellConfig) map[int]tw.Align { // Start with global alignment, preferring deprecated Formatting.Alignment effectiveGlobalAlign := config.Formatting.Alignment if effectiveGlobalAlign == tw.Empty || effectiveGlobalAlign == tw.Skip { effectiveGlobalAlign = config.Alignment.Global if config.Formatting.Alignment != tw.Empty && config.Formatting.Alignment != tw.Skip { t.logger.Warnf("Using deprecated CellFormatting.Alignment (%s). Migrate to CellConfig.Alignment.Global.", config.Formatting.Alignment) } } // Use per-column alignments, preferring deprecated ColumnAligns effectivePerColumn := config.ColumnAligns if len(effectivePerColumn) == 0 && len(config.Alignment.PerColumn) > 0 { effectivePerColumn = make([]tw.Align, len(config.Alignment.PerColumn)) copy(effectivePerColumn, config.Alignment.PerColumn) if len(config.ColumnAligns) > 0 { t.logger.Warnf("Using deprecated CellConfig.ColumnAligns (%v). Migrate to CellConfig.Alignment.PerColumn.", config.ColumnAligns) } } // Log input for debugging t.logger.Debugf("buildAligns INPUT: deprecated Formatting.Alignment=%s, deprecated ColumnAligns=%v, config.Alignment.Global=%s, config.Alignment.PerColumn=%v", config.Formatting.Alignment, config.ColumnAligns, config.Alignment.Global, config.Alignment.PerColumn) numColsToUse := t.getNumColsToUse() colAlignsResult := make(map[int]tw.Align) for i := 0; i < numColsToUse; i++ { currentAlign := effectiveGlobalAlign if i < len(effectivePerColumn) && effectivePerColumn[i] != tw.Empty && effectivePerColumn[i] != tw.Skip { currentAlign = effectivePerColumn[i] } // Skip validation here; rely on rendering to handle invalid alignments colAlignsResult[i] = currentAlign } t.logger.Debugf("Aligns built: %v (length %d)", colAlignsResult, len(colAlignsResult)) return colAlignsResult } // buildPadding constructs a map of column padding settings from configuration. // Parameter padding provides padding settings for the section. // Returns a map of column indices to padding settings. func (t *Table) buildPadding(padding tw.CellPadding) map[int]tw.Padding { numColsToUse := t.getNumColsToUse() colPadding := make(map[int]tw.Padding) for i := 0; i < numColsToUse; i++ { if i < len(padding.PerColumn) && padding.PerColumn[i].Paddable() { colPadding[i] = padding.PerColumn[i] } else { colPadding[i] = padding.Global } } t.logger.Debugf("Padding built: %v (length %d)", colPadding, len(colPadding)) return colPadding } // ensureInitialized initializes required fields before use. // No parameters are required. // No return value. func (t *Table) ensureInitialized() { if t.headerWidths == nil { t.headerWidths = tw.NewMapper[int, int]() } if t.rowWidths == nil { t.rowWidths = tw.NewMapper[int, int]() } if t.footerWidths == nil { t.footerWidths = tw.NewMapper[int, int]() } if t.renderer == nil { t.renderer = renderer.NewBlueprint() } t.logger.Debug("ensureInitialized called") } // finalizeHierarchicalMergeBlock sets Span and End for hierarchical merges. // Parameters include ctx, mctx, col, startRow, and endRow. // No return value. func (t *Table) finalizeHierarchicalMergeBlock(ctx *renderContext, mctx *mergeContext, col, startRow, endRow int) { if endRow < startRow { ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: Invalid block col %d, start %d > end %d", col, startRow, endRow) return } if startRow < 0 || endRow < 0 { ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: Negative row indices col %d, start %d, end %d", col, startRow, endRow) return } requiredLen := endRow + 1 if requiredLen > len(mctx.rowMerges) { ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: rowMerges slice too short (len %d) for endRow %d", len(mctx.rowMerges), endRow) return } if mctx.rowMerges[startRow] == nil { mctx.rowMerges[startRow] = make(map[int]tw.MergeState) } if mctx.rowMerges[endRow] == nil { mctx.rowMerges[endRow] = make(map[int]tw.MergeState) } finalSpan := (endRow - startRow) + 1 ctx.logger.Debugf("Finalizing H-merge block: col=%d, startRow=%d, endRow=%d, span=%d", col, startRow, endRow, finalSpan) startState := mctx.rowMerges[startRow][col] if startState.Hierarchical.Present && startState.Hierarchical.Start { startState.Hierarchical.Span = finalSpan startState.Hierarchical.End = finalSpan == 1 mctx.rowMerges[startRow][col] = startState ctx.logger.Debugf(" -> Updated start state: %+v", startState.Hierarchical) } else { ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: col %d, startRow %d was not marked as Present/Start? Current state: %+v. Attempting recovery.", col, startRow, startState.Hierarchical) startState.Hierarchical.Present = true startState.Hierarchical.Start = true startState.Hierarchical.Span = finalSpan startState.Hierarchical.End = finalSpan == 1 mctx.rowMerges[startRow][col] = startState } if endRow > startRow { endState := mctx.rowMerges[endRow][col] if endState.Hierarchical.Present && !endState.Hierarchical.Start { endState.Hierarchical.End = true endState.Hierarchical.Span = finalSpan mctx.rowMerges[endRow][col] = endState ctx.logger.Debugf(" -> Updated end state: %+v", endState.Hierarchical) } else { ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: col %d, endRow %d was not marked as Present/Continuation? Current state: %+v. Attempting recovery.", col, endRow, endState.Hierarchical) endState.Hierarchical.Present = true endState.Hierarchical.Start = false endState.Hierarchical.End = true endState.Hierarchical.Span = finalSpan mctx.rowMerges[endRow][col] = endState } } else { ctx.logger.Debugf(" -> Span is 1, startRow is also endRow.") } } // getLevel maps a position to its rendering level. // Parameter position specifies the section (Header, Row, Footer). // Returns the corresponding tw.Level (Header, Body, Footer). func (t *Table) getLevel(position tw.Position) tw.Level { switch position { case tw.Header: return tw.LevelHeader case tw.Row: return tw.LevelBody case tw.Footer: return tw.LevelFooter default: return tw.LevelBody } } // hasFooterElements checks if the footer has renderable elements. // No parameters are required. // Returns true if footer has content or padding, false otherwise. func (t *Table) hasFooterElements() bool { hasContent := len(t.footers) > 0 hasTopPadding := t.config.Footer.Padding.Global.Top != tw.Empty hasBottomPaddingConfig := t.config.Footer.Padding.Global.Bottom != tw.Empty || t.hasPerColumnBottomPadding() return hasContent || hasTopPadding || hasBottomPaddingConfig } // hasPerColumnBottomPadding checks for per-column bottom padding in footer. // No parameters are required. // Returns true if any per-column bottom padding is defined. func (t *Table) hasPerColumnBottomPadding() bool { if t.config.Footer.Padding.PerColumn == nil { return false } for _, pad := range t.config.Footer.Padding.PerColumn { if pad.Bottom != tw.Empty { return true } } return false } // Logger retrieves the table's logger instance. // No parameters are required. // Returns the ll.Logger instance used for debug tracing. func (t *Table) Logger() *ll.Logger { return t.logger } // Renderer retrieves the current renderer instance used by the table. // No parameters are required. // Returns the tw.Renderer interface instance. func (t *Table) Renderer() tw.Renderer { t.logger.Debug("Renderer requested") return t.renderer } // maxColumns calculates the maximum column count across sections. // No parameters are required. // Returns the highest number of columns found. func (t *Table) maxColumns() int { m := 0 if len(t.headers) > 0 && len(t.headers[0]) > m { m = len(t.headers[0]) } for _, row := range t.rows { if len(row) > 0 && len(row[0]) > m { m = len(row[0]) } } if len(t.footers) > 0 && len(t.footers[0]) > m { m = len(t.footers[0]) } t.logger.Debugf("Max columns: %d", m) return m } // printTopBottomCaption prints the table's caption at the specified top or bottom position. // It wraps the caption text to fit the table width or a user-defined width, aligns it according // to the specified alignment, and writes it to the provided writer. If the caption text is empty // or the spot is invalid, it logs the issue and returns without printing. The function handles // wrapping errors by falling back to splitting on newlines or using the original text. func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) { t.logger.Debugf("[printCaption Entry] Text=%q, Spot=%v (type %T), Align=%q, UserWidth=%d, ActualTableWidth=%d", t.caption.Text, t.caption.Spot, t.caption.Spot, t.caption.Align, t.caption.Width, actualTableWidth) currentCaptionSpot := t.caption.Spot isValidSpot := currentCaptionSpot >= tw.SpotTopLeft && currentCaptionSpot <= tw.SpotBottomRight if t.caption.Text == "" || !isValidSpot { t.logger.Debugf("[printCaption] Aborting: Text empty OR Spot invalid...") return } var captionWrapWidth int if t.caption.Width > 0 { captionWrapWidth = t.caption.Width t.logger.Debugf("[printCaption] Using user-defined caption.Width %d for wrapping.", captionWrapWidth) } else if actualTableWidth <= 4 { captionWrapWidth = twwidth.Width(t.caption.Text) t.logger.Debugf("[printCaption] Empty table, no user caption.Width: Using natural caption width %d.", captionWrapWidth) } else { captionWrapWidth = actualTableWidth t.logger.Debugf("[printCaption] Non-empty table, no user caption.Width: Using actualTableWidth %d for wrapping.", actualTableWidth) } if captionWrapWidth <= 0 { captionWrapWidth = 10 t.logger.Warnf("[printCaption] captionWrapWidth was %d (<=0). Setting to minimum %d.", captionWrapWidth, 10) } t.logger.Debugf("[printCaption] Final captionWrapWidth to be used by twwarp: %d", captionWrapWidth) wrappedCaptionLines, count := twwarp.WrapString(t.caption.Text, captionWrapWidth) if count == 0 { t.logger.Errorf("[printCaption] Error from twwarp.WrapString (width %d): %v. Text: %q", captionWrapWidth, count, t.caption.Text) if strings.Contains(t.caption.Text, "\n") { wrappedCaptionLines = strings.Split(t.caption.Text, "\n") } else { wrappedCaptionLines = []string{t.caption.Text} } t.logger.Debugf("[printCaption] Fallback: using %d lines from original text.", len(wrappedCaptionLines)) } if len(wrappedCaptionLines) == 0 && t.caption.Text != "" { t.logger.Warn("[printCaption] Wrapping resulted in zero lines for non-empty text. Using fallback.") if strings.Contains(t.caption.Text, "\n") { wrappedCaptionLines = strings.Split(t.caption.Text, "\n") } else { wrappedCaptionLines = []string{t.caption.Text} } } else if t.caption.Text != "" { t.logger.Debugf("[printCaption] Wrapped caption into %d lines: %v", len(wrappedCaptionLines), wrappedCaptionLines) } paddingTargetWidth := actualTableWidth if t.caption.Width > 0 { paddingTargetWidth = t.caption.Width } else if actualTableWidth <= 4 { paddingTargetWidth = captionWrapWidth } t.logger.Debugf("[printCaption] Final paddingTargetWidth for tw.Pad: %d", paddingTargetWidth) for i, line := range wrappedCaptionLines { align := t.caption.Align if align == "" || align == tw.AlignDefault || align == tw.AlignNone { switch t.caption.Spot { case tw.SpotTopLeft, tw.SpotBottomLeft: align = tw.AlignLeft case tw.SpotTopRight, tw.SpotBottomRight: align = tw.AlignRight default: align = tw.AlignCenter } t.logger.Debugf("[printCaption] Line %d: Alignment defaulted to %s based on Spot %v", i, align, t.caption.Spot) } paddedLine := tw.Pad(line, " ", paddingTargetWidth, align) t.logger.Debugf("[printCaption] Printing line %d: InputLine=%q, Align=%s, PaddingTargetWidth=%d, PaddedLine=%q", i, line, align, paddingTargetWidth, paddedLine) w.Write([]byte(paddedLine)) w.Write([]byte(tw.NewLine)) } t.logger.Debugf("[printCaption] Finished printing all caption lines.") } // prepareContent processes cell content with formatting and wrapping. // Parameters include cells to process and config for formatting rules. // Returns a slice of string slices representing processed lines. func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string { isStreaming := t.config.Stream.Enable && t.hasPrinted t.logger.Debugf("prepareContent: Processing cells=%v (streaming: %v)", cells, isStreaming) initialInputCellCount := len(cells) result := make([][]string, 0) effectiveNumCols := initialInputCellCount if isStreaming { if t.streamNumCols > 0 { effectiveNumCols = t.streamNumCols t.logger.Debugf("prepareContent: Streaming mode, using fixed streamNumCols: %d", effectiveNumCols) if len(cells) != effectiveNumCols { t.logger.Warnf("prepareContent: Streaming mode, input cell count (%d) does not match streamNumCols (%d). Input cells will be padded/truncated.", len(cells), effectiveNumCols) if len(cells) < effectiveNumCols { paddedCells := make([]string, effectiveNumCols) copy(paddedCells, cells) for i := len(cells); i < effectiveNumCols; i++ { paddedCells[i] = tw.Empty } cells = paddedCells } else if len(cells) > effectiveNumCols { cells = cells[:effectiveNumCols] } } } else { t.logger.Warnf("prepareContent: Streaming mode enabled but streamNumCols is 0. Using input cell count %d. Stream widths may not be available.", effectiveNumCols) } } if t.config.MaxWidth > 0 && !t.config.Widths.Constrained() { if effectiveNumCols > 0 { derivedSectionGlobalMaxWidth := int(math.Floor(float64(t.config.MaxWidth) / float64(effectiveNumCols))) config.ColMaxWidths.Global = derivedSectionGlobalMaxWidth t.logger.Debugf("prepareContent: Table MaxWidth %d active and t.config.Widths not constrained. "+ "Derived section ColMaxWidths.Global: %d for %d columns. This will be used by calculateContentMaxWidth if no higher priority constraints exist.", t.config.MaxWidth, config.ColMaxWidths.Global, effectiveNumCols) } } for i := 0; i < effectiveNumCols; i++ { cellContent := "" if i < len(cells) { cellContent = cells[i] } else { cellContent = tw.Empty } cellContent = t.Trimmer(cellContent) colPad := config.Padding.Global if i < len(config.Padding.PerColumn) && config.Padding.PerColumn[i].Paddable() { colPad = config.Padding.PerColumn[i] } padLeftWidth := twwidth.Width(colPad.Left) padRightWidth := twwidth.Width(colPad.Right) effectiveContentMaxWidth := t.calculateContentMaxWidth(i, config, padLeftWidth, padRightWidth, isStreaming) if config.Formatting.AutoFormat.Enabled() { cellContent = tw.Title(strings.Join(tw.SplitCamelCase(cellContent), tw.Space)) } lines := strings.Split(cellContent, "\n") finalLinesForCell := make([]string, 0) for _, line := range lines { if effectiveContentMaxWidth > 0 { switch config.Formatting.AutoWrap { case tw.WrapNormal: var wrapped []string if t.config.Behavior.TrimSpace.Enabled() { wrapped, _ = twwarp.WrapString(line, effectiveContentMaxWidth) } else { wrapped, _ = twwarp.WrapStringWithSpaces(line, effectiveContentMaxWidth) } finalLinesForCell = append(finalLinesForCell, wrapped...) case tw.WrapTruncate: if twwidth.Width(line) > effectiveContentMaxWidth { ellipsisWidth := twwidth.Width(tw.CharEllipsis) if effectiveContentMaxWidth >= ellipsisWidth { finalLinesForCell = append(finalLinesForCell, twwidth.Truncate(line, effectiveContentMaxWidth-ellipsisWidth, tw.CharEllipsis)) } else { finalLinesForCell = append(finalLinesForCell, twwidth.Truncate(line, effectiveContentMaxWidth, "")) } } else { finalLinesForCell = append(finalLinesForCell, line) } case tw.WrapBreak: wrapped := make([]string, 0) currentLine := line breakCharWidth := twwidth.Width(tw.CharBreak) for twwidth.Width(currentLine) > effectiveContentMaxWidth { targetWidth := effectiveContentMaxWidth - breakCharWidth if targetWidth < 0 { targetWidth = 0 } breakPoint := tw.BreakPoint(currentLine, targetWidth) runes := []rune(currentLine) if breakPoint <= 0 || breakPoint > len(runes) { t.logger.Warnf("prepareContent: WrapBreak - Invalid BreakPoint %d for line '%s' at width %d. Attempting manual break.", breakPoint, currentLine, targetWidth) actualBreakRuneCount := 0 tempWidth := 0 for charIdx, r := range runes { runeStr := string(r) rw := twwidth.Width(runeStr) if tempWidth+rw > targetWidth && charIdx > 0 { break } tempWidth += rw actualBreakRuneCount = charIdx + 1 if tempWidth >= targetWidth && charIdx == 0 { break } } if actualBreakRuneCount == 0 && len(runes) > 0 { actualBreakRuneCount = 1 } if actualBreakRuneCount > 0 && actualBreakRuneCount <= len(runes) { wrapped = append(wrapped, string(runes[:actualBreakRuneCount])+tw.CharBreak) currentLine = string(runes[actualBreakRuneCount:]) } else { t.logger.Warnf("prepareContent: WrapBreak - Cannot break line '%s'. Adding as is.", currentLine) wrapped = append(wrapped, currentLine) currentLine = "" break } } else { wrapped = append(wrapped, string(runes[:breakPoint])+tw.CharBreak) currentLine = string(runes[breakPoint:]) } } if twwidth.Width(currentLine) > 0 { wrapped = append(wrapped, currentLine) } if len(wrapped) == 0 && twwidth.Width(line) > 0 && len(finalLinesForCell) == 0 { finalLinesForCell = append(finalLinesForCell, line) } else { finalLinesForCell = append(finalLinesForCell, wrapped...) } default: finalLinesForCell = append(finalLinesForCell, line) } } else { finalLinesForCell = append(finalLinesForCell, line) } } for len(result) < len(finalLinesForCell) { newRow := make([]string, effectiveNumCols) for j := range newRow { newRow[j] = tw.Empty } result = append(result, newRow) } for j := 0; j < len(result); j++ { cellLineContent := tw.Empty if j < len(finalLinesForCell) { cellLineContent = finalLinesForCell[j] } if i < len(result[j]) { result[j][i] = cellLineContent } else { t.logger.Warnf("prepareContent: Column index %d out of bounds (%d) during result matrix population. EffectiveNumCols: %d. This indicates a logic error.", i, len(result[j]), effectiveNumCols) } } } t.logger.Debugf("prepareContent: Content prepared, result %d lines.", len(result)) return result } // prepareContexts initializes rendering and merge contexts. // No parameters are required. // Returns renderContext, mergeContext, and an error if initialization fails. func (t *Table) prepareContexts() (*renderContext, *mergeContext, error) { numOriginalCols := t.maxColumns() t.logger.Debugf("prepareContexts: Original number of columns: %d", numOriginalCols) ctx := &renderContext{ table: t, renderer: t.renderer, cfg: t.renderer.Config(), numCols: numOriginalCols, widths: map[tw.Position]tw.Mapper[int, int]{ tw.Header: tw.NewMapper[int, int](), tw.Row: tw.NewMapper[int, int](), tw.Footer: tw.NewMapper[int, int](), }, logger: t.logger, } isEmpty, visibleCount := t.getEmptyColumnInfo(numOriginalCols) ctx.emptyColumns = isEmpty ctx.visibleColCount = visibleCount mctx := &mergeContext{ headerMerges: make(map[int]tw.MergeState), rowMerges: make([]map[int]tw.MergeState, len(t.rows)), footerMerges: make(map[int]tw.MergeState), horzMerges: make(map[tw.Position]map[int]bool), } for i := range mctx.rowMerges { mctx.rowMerges[i] = make(map[int]tw.MergeState) } ctx.headerLines = t.headers ctx.rowLines = t.rows ctx.footerLines = t.footers if err := t.calculateAndNormalizeWidths(ctx); err != nil { t.logger.Debugf("Error during initial width calculation: %v", err) return nil, nil, err } t.logger.Debugf("Initial normalized widths (before hiding): H=%v, R=%v, F=%v", ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer]) preparedHeaderLines, headerMerges, _ := t.prepareWithMerges(ctx.headerLines, t.config.Header, tw.Header) ctx.headerLines = preparedHeaderLines mctx.headerMerges = headerMerges processedRowLines := make([][][]string, len(ctx.rowLines)) for i, row := range ctx.rowLines { if mctx.rowMerges[i] == nil { mctx.rowMerges[i] = make(map[int]tw.MergeState) } processedRowLines[i], mctx.rowMerges[i], _ = t.prepareWithMerges(row, t.config.Row, tw.Row) } ctx.rowLines = processedRowLines t.applyHorizontalMergeWidths(tw.Header, ctx, mctx.headerMerges) if t.config.Row.Formatting.MergeMode&tw.MergeVertical != 0 { t.applyVerticalMerges(ctx, mctx) } if t.config.Row.Formatting.MergeMode&tw.MergeHierarchical != 0 { t.applyHierarchicalMerges(ctx, mctx) } t.prepareFooter(ctx, mctx) t.logger.Debugf("Footer prepared. Widths before hiding: H=%v, R=%v, F=%v", ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer]) if t.config.Behavior.AutoHide.Enabled() { t.logger.Debugf("Applying AutoHide: Adjusting widths for empty columns.") if ctx.emptyColumns == nil { t.logger.Debugf("Warning: ctx.emptyColumns is nil during width adjustment.") } else if len(ctx.emptyColumns) != ctx.numCols { t.logger.Debugf("Warning: Length mismatch between emptyColumns (%d) and numCols (%d). Skipping adjustment.", len(ctx.emptyColumns), ctx.numCols) } else { for colIdx := 0; colIdx < ctx.numCols; colIdx++ { if ctx.emptyColumns[colIdx] { t.logger.Debugf("AutoHide: Hiding column %d by setting width to 0.", colIdx) ctx.widths[tw.Header].Set(colIdx, 0) ctx.widths[tw.Row].Set(colIdx, 0) ctx.widths[tw.Footer].Set(colIdx, 0) } } t.logger.Debugf("Widths after AutoHide adjustment: H=%v, R=%v, F=%v", ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer]) } } else { t.logger.Debugf("AutoHide is disabled, skipping width adjustment.") } t.logger.Debugf("prepareContexts completed all stages.") return ctx, mctx, nil } // prepareFooter processes footer content and applies merges. // Parameters ctx and mctx hold rendering and merge state. // No return value. func (t *Table) prepareFooter(ctx *renderContext, mctx *mergeContext) { if len(t.footers) == 0 { ctx.logger.Debugf("Skipping footer preparation - no footer data") if ctx.widths[tw.Footer] == nil { ctx.widths[tw.Footer] = tw.NewMapper[int, int]() } numCols := ctx.numCols for i := 0; i < numCols; i++ { ctx.widths[tw.Footer].Set(i, ctx.widths[tw.Row].Get(i)) } t.logger.Debug("Initialized empty footer widths based on row widths: %v", ctx.widths[tw.Footer]) ctx.footerPrepared = true return } t.logger.Debugf("Preparing footer with merge mode: %d", t.config.Footer.Formatting.MergeMode) preparedLines, mergeStates, _ := t.prepareWithMerges(t.footers, t.config.Footer, tw.Footer) t.footers = preparedLines mctx.footerMerges = mergeStates ctx.footerLines = t.footers t.logger.Debugf("Base footer widths (normalized from rows/header): %v", ctx.widths[tw.Footer]) t.applyHorizontalMergeWidths(tw.Footer, ctx, mctx.footerMerges) ctx.footerPrepared = true t.logger.Debugf("Footer preparation completed. Final footer widths: %v", ctx.widths[tw.Footer]) } // prepareWithMerges processes content and detects horizontal merges. // Parameters include content, config, and position (Header, Row, Footer). // Returns processed lines, merge states, and horizontal merge map. func (t *Table) prepareWithMerges(content [][]string, config tw.CellConfig, position tw.Position) ([][]string, map[int]tw.MergeState, map[int]bool) { t.logger.Debugf("PrepareWithMerges START: position=%s, mergeMode=%d", position, config.Formatting.MergeMode) if len(content) == 0 { t.logger.Debugf("PrepareWithMerges END: No content.") return content, nil, nil } numCols := 0 if len(content) > 0 && len(content[0]) > 0 { // Assumes content[0] exists and has items numCols = len(content[0]) } else { // Fallback if first line is empty or content is empty for _, line := range content { // Find max columns from any line if len(line) > numCols { numCols = len(line) } } if numCols == 0 { // If still 0, try table-wide max (batch mode context) numCols = t.maxColumns() } } if numCols == 0 { t.logger.Debugf("PrepareWithMerges END: numCols is zero.") return content, nil, nil } horzMergeMap := make(map[int]bool) // Tracks if a column is part of any horizontal merge for this logical row mergeMap := make(map[int]tw.MergeState) // Final merge states for this logical row // Ensure all lines in 'content' are padded to numCols for consistent processing // This result is what will be modified and returned. result := make([][]string, len(content)) for i := range content { result[i] = padLine(content[i], numCols) } if config.Formatting.MergeMode&tw.MergeHorizontal != 0 { t.logger.Debugf("Checking for horizontal merges (logical cell comparison) for %d visual lines, %d columns", len(content), numCols) // Special handling for footer lead merge (often for "TOTAL" spanning empty cells) // This logic only applies if it's a footer and typically to the first (often only) visual line. if position == tw.Footer && len(content) > 0 { lineIdx := 0 // Assume footer lead merge applies to the first visual line primarily originalLine := padLine(content[lineIdx], numCols) // Use original content for decision currentLineResult := result[lineIdx] // Modify the result line firstContentIdx := -1 var firstContent string for c := 0; c < numCols; c++ { if c >= len(originalLine) { break } trimmedVal := t.Trimmer(originalLine[c]) if trimmedVal != "" && trimmedVal != "-" { // "-" is often a placeholder not to merge over firstContentIdx = c firstContent = originalLine[c] // Store the raw content for placement break } else if trimmedVal == "-" { // Stop if we hit a hard non-mergeable placeholder break } } if firstContentIdx > 0 { // If content starts after the first column span := firstContentIdx + 1 // Merge from col 0 up to and including firstContentIdx startCol := 0 allEmptyBefore := true for c := 0; c < firstContentIdx; c++ { originalLine[c] = t.Trimmer(originalLine[c]) if c >= len(originalLine) || originalLine[c] != "" { allEmptyBefore = false break } } if allEmptyBefore { t.logger.Debugf("Footer lead-merge applied line %d: content '%s' from col %d moved to col %d, span %d", lineIdx, firstContent, firstContentIdx, startCol, span) if startCol < len(currentLineResult) { currentLineResult[startCol] = firstContent // Place the original content } for k := startCol + 1; k < startCol+span; k++ { // Clear out other cells in the span if k < len(currentLineResult) { currentLineResult[k] = tw.Empty } } // Update mergeMap for all visual lines of this logical row for visualLine := 0; visualLine < len(result); visualLine++ { // Only apply the data move to the line where it was detected, // but the merge state should apply to the logical cell (all its visual lines). if visualLine != lineIdx { // For other visual lines, just clear the cells in the span if startCol < len(result[visualLine]) { result[visualLine][startCol] = tw.Empty // Typically empty for other lines in a lead merge } for k := startCol + 1; k < startCol+span; k++ { if k < len(result[visualLine]) { result[visualLine][k] = tw.Empty } } } } // Set merge state for the starting column startState := mergeMap[startCol] startState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: true, End: (span == 1)} mergeMap[startCol] = startState horzMergeMap[startCol] = true // Mark this column as processed by a merge // Set merge state for subsequent columns in the span for k := startCol + 1; k < startCol+span; k++ { colState := mergeMap[k] colState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: false, End: k == startCol+span-1} mergeMap[k] = colState horzMergeMap[k] = true // Mark as processed } } } } // Standard horizontal merge logic based on full logical cell content col := 0 for col < numCols { if horzMergeMap[col] { // If already part of a footer lead-merge, skip col++ continue } // Get full content of logical cell 'col' var currentLogicalCellContentBuilder strings.Builder for lineIdx := 0; lineIdx < len(content); lineIdx++ { if col < len(content[lineIdx]) { currentLogicalCellContentBuilder.WriteString(content[lineIdx][col]) } } currentLogicalCellTrimmed := t.Trimmer(currentLogicalCellContentBuilder.String()) if currentLogicalCellTrimmed == "" || currentLogicalCellTrimmed == "-" { col++ continue } span := 1 for nextCol := col + 1; nextCol < numCols; nextCol++ { if horzMergeMap[nextCol] { // Don't merge into an already merged (e.g. footer lead) column break } var nextLogicalCellContentBuilder strings.Builder for lineIdx := 0; lineIdx < len(content); lineIdx++ { if nextCol < len(content[lineIdx]) { nextLogicalCellContentBuilder.WriteString(content[lineIdx][nextCol]) } } nextLogicalCellTrimmed := t.Trimmer(nextLogicalCellContentBuilder.String()) if currentLogicalCellTrimmed == nextLogicalCellTrimmed && nextLogicalCellTrimmed != "-" { span++ } else { break } } if span > 1 { t.logger.Debugf("Standard horizontal merge (logical cell): startCol %d, span %d for content '%s'", col, span, currentLogicalCellTrimmed) startState := mergeMap[col] startState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: true, End: (span == 1)} mergeMap[col] = startState horzMergeMap[col] = true // For all visual lines, clear out the content of the merged-over cells for lineIdx := 0; lineIdx < len(result); lineIdx++ { for k := col + 1; k < col+span; k++ { if k < len(result[lineIdx]) { result[lineIdx][k] = tw.Empty } } } // Set merge state for subsequent columns in the span for k := col + 1; k < col+span; k++ { colState := mergeMap[k] colState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: false, End: k == col+span-1} mergeMap[k] = colState horzMergeMap[k] = true } col += span } else { col++ } } } t.logger.Debugf("PrepareWithMerges END: position=%s, lines=%d, mergeMapH: %v", position, len(result), func() map[int]tw.MergeStateOption { m := make(map[int]tw.MergeStateOption) for k, v := range mergeMap { m[k] = v.Horizontal } return m }()) return result, mergeMap, horzMergeMap } // render generates the table output using the configured renderer. // No parameters are required. // Returns an error if rendering fails in any section. func (t *Table) render() error { t.ensureInitialized() if t.config.Stream.Enable { t.logger.Warn("Render() called in streaming mode. Use Start/Append/Close methods instead.") return errors.New("render called in streaming mode; use Start/Append/Close") } // Calculate and cache numCols for THIS batch render pass t.batchRenderNumCols = t.maxColumns() // Calculate ONCE t.isBatchRenderNumColsSet = true // Mark the cache as active for this render pass t.logger.Debugf("Render(): Set batchRenderNumCols to %d and isBatchRenderNumColsSet to true.", t.batchRenderNumCols) defer func() { t.isBatchRenderNumColsSet = false // t.batchRenderNumCols = 0; // Optional: reset to 0, or leave as is. // Since isBatchRenderNumColsSet is false, its value won't be used by getNumColsToUse. t.logger.Debugf("Render(): Cleared isBatchRenderNumColsSet to false (batchRenderNumCols was %d).", t.batchRenderNumCols) }() hasCaption := t.caption.Text != "" && t.caption.Spot != tw.SpotNone isTopOrBottomCaption := hasCaption && (t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotBottomRight) var tableStringBuffer *strings.Builder targetWriter := t.writer originalWriter := t.writer // Save original writer for restoration if needed if isTopOrBottomCaption { tableStringBuffer = &strings.Builder{} targetWriter = tableStringBuffer t.logger.Debugf("Top/Bottom caption detected. Rendering table core to buffer first.") } else { t.logger.Debugf("No caption detected. Rendering table core directly to writer.") } //Render Table Core t.writer = targetWriter ctx, mctx, err := t.prepareContexts() if err != nil { t.writer = originalWriter t.logger.Errorf("prepareContexts failed: %v", err) return errors.Newf("failed to prepare table contexts").Wrap(err) } if err := ctx.renderer.Start(t.writer); err != nil { t.writer = originalWriter t.logger.Errorf("Renderer Start() error: %v", err) return errors.Newf("renderer start failed").Wrap(err) } renderError := false var firstRenderErr error renderFuncs := []func(*renderContext, *mergeContext) error{ t.renderHeader, t.renderRow, t.renderFooter, } for i, renderFn := range renderFuncs { sectionName := []string{"Header", "Row", "Footer"}[i] if renderErr := renderFn(ctx, mctx); renderErr != nil { t.logger.Errorf("Renderer section error (%s): %v", sectionName, renderErr) if !renderError { firstRenderErr = errors.Newf("failed to render %s section", sectionName).Wrap(renderErr) } renderError = true break } } if closeErr := ctx.renderer.Close(); closeErr != nil { t.logger.Errorf("Renderer Close() error: %v", closeErr) if !renderError { firstRenderErr = errors.Newf("renderer close failed").Wrap(closeErr) } renderError = true } t.writer = originalWriter // Restore original writer if renderError { return firstRenderErr // Return error from core rendering if any } //Caption Handling & Final Output --- if isTopOrBottomCaption { renderedTableContent := tableStringBuffer.String() t.logger.Debugf("[Render] Table core buffer length: %d", len(renderedTableContent)) // Check if the buffer is empty AND borders are enabled shouldHaveBorders := t.renderer != nil && (t.renderer.Config().Borders.Top.Enabled() || t.renderer.Config().Borders.Bottom.Enabled()) if len(renderedTableContent) == 0 && shouldHaveBorders { var sb strings.Builder if t.renderer.Config().Borders.Top.Enabled() { sb.WriteString("+--+") sb.WriteString(t.newLine) } if t.renderer.Config().Borders.Bottom.Enabled() { sb.WriteString("+--+") } renderedTableContent = sb.String() t.logger.Warnf("[Render] Table buffer was empty despite enabled borders. Manually generated minimal output: %q", renderedTableContent) } actualTableWidth := 0 trimmedBuffer := strings.TrimRight(renderedTableContent, "\r\n \t") for _, line := range strings.Split(trimmedBuffer, "\n") { w := twwidth.Width(line) if w > actualTableWidth { actualTableWidth = w } } t.logger.Debugf("[Render] Calculated actual table width: %d (from content: %q)", actualTableWidth, renderedTableContent) isTopCaption := t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotTopRight if isTopCaption { t.logger.Debugf("[Render] Printing Top Caption.") t.printTopBottomCaption(t.writer, actualTableWidth) } if len(renderedTableContent) > 0 { t.logger.Debugf("[Render] Printing table content (length %d) to final writer.", len(renderedTableContent)) t.writer.Write([]byte(renderedTableContent)) if !isTopCaption && t.caption.Text != "" && !strings.HasSuffix(renderedTableContent, t.newLine) { t.writer.Write([]byte(tw.NewLine)) t.logger.Debugf("[Render] Added trailing newline after table content before bottom caption.") } } else { t.logger.Debugf("[Render] No table content (original buffer or generated) to print.") } if !isTopCaption { t.logger.Debugf("[Render] Calling printTopBottomCaption for Bottom Caption. Width: %d", actualTableWidth) t.printTopBottomCaption(t.writer, actualTableWidth) t.logger.Debugf("[Render] Returned from printTopBottomCaption for Bottom Caption.") } } t.hasPrinted = true t.logger.Info("Render() completed.") return nil // Success } // renderFooter renders the table's footer section with borders and padding. // Parameters ctx and mctx hold rendering and merge state. // Returns an error if rendering fails. func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error { if !ctx.footerPrepared { t.prepareFooter(ctx, mctx) } f := ctx.renderer cfg := ctx.cfg hasContent := len(ctx.footerLines) > 0 hasTopPadding := t.config.Footer.Padding.Global.Top != tw.Empty hasBottomPaddingConfig := t.config.Footer.Padding.Global.Bottom != tw.Empty || t.hasPerColumnBottomPadding() hasAnyFooterElement := hasContent || hasTopPadding || hasBottomPaddingConfig if !hasAnyFooterElement { hasContentAbove := len(ctx.rowLines) > 0 || len(ctx.headerLines) > 0 if hasContentAbove && cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled() { ctx.logger.Debugf("Footer is empty, rendering table bottom border based on last row/header") var lastLineAboveCtx *helperContext var lastLineAligns map[int]tw.Align var lastLinePadding map[int]tw.Padding if len(ctx.rowLines) > 0 { lastRowIdx := len(ctx.rowLines) - 1 lastRowLineIdx := -1 var lastRowLine []string if lastRowIdx >= 0 && len(ctx.rowLines[lastRowIdx]) > 0 { lastRowLineIdx = len(ctx.rowLines[lastRowIdx]) - 1 lastRowLine = padLine(ctx.rowLines[lastRowIdx][lastRowLineIdx], ctx.numCols) } else { lastRowLine = make([]string, ctx.numCols) } lastLineAboveCtx = &helperContext{ position: tw.Row, rowIdx: lastRowIdx, lineIdx: lastRowLineIdx, line: lastRowLine, location: tw.LocationEnd, } lastLineAligns = t.buildAligns(t.config.Row) lastLinePadding = t.buildPadding(t.config.Row.Padding) } else { lastHeaderLineIdx := -1 var lastHeaderLine []string if len(ctx.headerLines) > 0 { lastHeaderLineIdx = len(ctx.headerLines) - 1 lastHeaderLine = padLine(ctx.headerLines[lastHeaderLineIdx], ctx.numCols) } else { lastHeaderLine = make([]string, ctx.numCols) } lastLineAboveCtx = &helperContext{ position: tw.Header, rowIdx: 0, lineIdx: lastHeaderLineIdx, line: lastHeaderLine, location: tw.LocationEnd, } lastLineAligns = t.buildAligns(t.config.Header) lastLinePadding = t.buildPadding(t.config.Header.Padding) } resp := t.buildCellContexts(ctx, mctx, lastLineAboveCtx, lastLineAligns, lastLinePadding) ctx.logger.Debugf("Bottom border: Using Widths=%v", ctx.widths[tw.Row]) f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: ctx.widths[tw.Row], Current: resp.cells, Previous: resp.prevCells, Position: lastLineAboveCtx.position, Location: tw.LocationEnd, ColMaxWidths: t.getColMaxWidths(tw.Footer), }, Level: tw.LevelFooter, IsSubRow: false, }) } else { ctx.logger.Debugf("Footer is empty and no content above or borders disabled, skipping footer render") } return nil } ctx.logger.Debugf("Rendering footer section (has elements)") hasContentAbove := len(ctx.rowLines) > 0 || len(ctx.headerLines) > 0 colAligns := t.buildAligns(t.config.Footer) colPadding := t.buildPadding(t.config.Footer.Padding) hctx := &helperContext{position: tw.Footer} // Declare paddingLineContentForContext with a default value paddingLineContentForContext := make([]string, ctx.numCols) if hasContentAbove && cfg.Settings.Lines.ShowFooterLine.Enabled() && !hasTopPadding && len(ctx.footerLines) > 0 { ctx.logger.Debugf("Rendering footer separator line") var lastLineAboveCtx *helperContext var lastLineAligns map[int]tw.Align var lastLinePadding map[int]tw.Padding var lastLinePosition tw.Position if len(ctx.rowLines) > 0 { lastRowIdx := len(ctx.rowLines) - 1 lastRowLineIdx := -1 var lastRowLine []string if lastRowIdx >= 0 && len(ctx.rowLines[lastRowIdx]) > 0 { lastRowLineIdx = len(ctx.rowLines[lastRowIdx]) - 1 lastRowLine = padLine(ctx.rowLines[lastRowIdx][lastRowLineIdx], ctx.numCols) } else { lastRowLine = make([]string, ctx.numCols) } lastLineAboveCtx = &helperContext{ position: tw.Row, rowIdx: lastRowIdx, lineIdx: lastRowLineIdx, line: lastRowLine, location: tw.LocationMiddle, } lastLineAligns = t.buildAligns(t.config.Row) lastLinePadding = t.buildPadding(t.config.Row.Padding) lastLinePosition = tw.Row } else { lastHeaderLineIdx := -1 var lastHeaderLine []string if len(ctx.headerLines) > 0 { lastHeaderLineIdx = len(ctx.headerLines) - 1 lastHeaderLine = padLine(ctx.headerLines[lastHeaderLineIdx], ctx.numCols) } else { lastHeaderLine = make([]string, ctx.numCols) } lastLineAboveCtx = &helperContext{ position: tw.Header, rowIdx: 0, lineIdx: lastHeaderLineIdx, line: lastHeaderLine, location: tw.LocationMiddle, } lastLineAligns = t.buildAligns(t.config.Header) lastLinePadding = t.buildPadding(t.config.Header.Padding) lastLinePosition = tw.Header } resp := t.buildCellContexts(ctx, mctx, lastLineAboveCtx, lastLineAligns, lastLinePadding) var nextCells map[int]tw.CellContext if hasContent { nextCells = make(map[int]tw.CellContext) for j, cellData := range padLine(ctx.footerLines[0], ctx.numCols) { mergeState := tw.MergeState{} if mctx.footerMerges != nil { mergeState = mctx.footerMerges[j] } nextCells[j] = tw.CellContext{Data: cellData, Merge: mergeState, Width: ctx.widths[tw.Footer].Get(j)} } } ctx.logger.Debugf("Footer separator: Using Widths=%v", ctx.widths[tw.Row]) f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: ctx.widths[tw.Row], Current: resp.cells, Previous: resp.prevCells, Next: nextCells, Position: lastLinePosition, Location: tw.LocationMiddle, ColMaxWidths: t.getColMaxWidths(tw.Footer), }, Level: tw.LevelFooter, IsSubRow: false, HasFooter: true, }) } if hasTopPadding { hctx.rowIdx = 0 hctx.lineIdx = -1 if !(hasContentAbove && cfg.Settings.Lines.ShowFooterLine.Enabled()) { hctx.location = tw.LocationFirst } else { hctx.location = tw.LocationMiddle } hctx.line = t.buildPaddingLineContents(t.config.Footer.Padding.Global.Top, ctx.widths[tw.Footer], ctx.numCols, mctx.footerMerges) ctx.logger.Debugf("Calling renderPadding for Footer Top Padding line: %v (loc: %v)", hctx.line, hctx.location) if err := t.renderPadding(ctx, mctx, hctx, t.config.Footer.Padding.Global.Top); err != nil { return err } } lastRenderedLineIdx := -2 if hasTopPadding { lastRenderedLineIdx = -1 } for i, line := range ctx.footerLines { hctx.rowIdx = 0 hctx.lineIdx = i hctx.line = padLine(line, ctx.numCols) isFirstContentLine := i == 0 isLastContentLine := i == len(ctx.footerLines)-1 if isFirstContentLine && !hasTopPadding && !(hasContentAbove && cfg.Settings.Lines.ShowFooterLine.Enabled()) { hctx.location = tw.LocationFirst } else if isLastContentLine && !hasBottomPaddingConfig { hctx.location = tw.LocationEnd } else { hctx.location = tw.LocationMiddle } ctx.logger.Debugf("Rendering footer content line %d with location %v", i, hctx.location) if err := t.renderLine(ctx, mctx, hctx, colAligns, colPadding); err != nil { return err } lastRenderedLineIdx = i } if hasBottomPaddingConfig { paddingLineContentForContext = make([]string, ctx.numCols) formattedPaddingCells := make([]string, ctx.numCols) var representativePadChar string = " " ctx.logger.Debugf("Constructing Footer Bottom Padding line content strings") for j := 0; j < ctx.numCols; j++ { colWd := ctx.widths[tw.Footer].Get(j) mergeState := tw.MergeState{} if mctx.footerMerges != nil { if state, ok := mctx.footerMerges[j]; ok { mergeState = state } } if mergeState.Horizontal.Present && !mergeState.Horizontal.Start { paddingLineContentForContext[j] = "" formattedPaddingCells[j] = "" continue } padChar := " " if j < len(t.config.Footer.Padding.PerColumn) && t.config.Footer.Padding.PerColumn[j].Bottom != tw.Empty { padChar = t.config.Footer.Padding.PerColumn[j].Bottom } else if t.config.Footer.Padding.Global.Bottom != tw.Empty { padChar = t.config.Footer.Padding.Global.Bottom } paddingLineContentForContext[j] = padChar if j == 0 || representativePadChar == " " { representativePadChar = padChar } padWidth := twwidth.Width(padChar) if padWidth < 1 { padWidth = 1 } repeatCount := 0 if colWd > 0 && padWidth > 0 { repeatCount = colWd / padWidth } if colWd > 0 && repeatCount < 1 && padChar != " " { repeatCount = 1 } if colWd == 0 { repeatCount = 0 } rawPaddingContent := strings.Repeat(padChar, repeatCount) currentWd := twwidth.Width(rawPaddingContent) if currentWd < colWd { rawPaddingContent += strings.Repeat(" ", colWd-currentWd) } if currentWd > colWd && colWd > 0 { rawPaddingContent = twwidth.Truncate(rawPaddingContent, colWd) } if colWd == 0 { rawPaddingContent = "" } formattedPaddingCells[j] = rawPaddingContent } ctx.logger.Debugf("Manually rendering Footer Bottom Padding line (char like '%s')", representativePadChar) var paddingLineOutput strings.Builder if cfg.Borders.Left.Enabled() { paddingLineOutput.WriteString(cfg.Symbols.Column()) } for colIdx := 0; colIdx < ctx.numCols; { if colIdx > 0 && cfg.Settings.Separators.BetweenColumns.Enabled() { shouldAddSeparator := true if prevMergeState, ok := mctx.footerMerges[colIdx-1]; ok { if prevMergeState.Horizontal.Present && !prevMergeState.Horizontal.End { shouldAddSeparator = false } } if shouldAddSeparator { paddingLineOutput.WriteString(cfg.Symbols.Column()) } } if colIdx < len(formattedPaddingCells) { paddingLineOutput.WriteString(formattedPaddingCells[colIdx]) } currentMergeState := tw.MergeState{} if mctx.footerMerges != nil { if state, ok := mctx.footerMerges[colIdx]; ok { currentMergeState = state } } if currentMergeState.Horizontal.Present && currentMergeState.Horizontal.Start { colIdx += currentMergeState.Horizontal.Span } else { colIdx++ } } if cfg.Borders.Right.Enabled() { paddingLineOutput.WriteString(cfg.Symbols.Column()) } paddingLineOutput.WriteString(t.newLine) t.writer.Write([]byte(paddingLineOutput.String())) ctx.logger.Debugf("Manually rendered Footer Bottom Padding line: %s", strings.TrimSuffix(paddingLineOutput.String(), t.newLine)) hctx.rowIdx = 0 hctx.lineIdx = len(ctx.footerLines) hctx.line = paddingLineContentForContext hctx.location = tw.LocationEnd lastRenderedLineIdx = hctx.lineIdx } if cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled() { ctx.logger.Debugf("Rendering final table bottom border") if lastRenderedLineIdx == len(ctx.footerLines) { hctx.rowIdx = 0 hctx.lineIdx = lastRenderedLineIdx hctx.line = paddingLineContentForContext hctx.location = tw.LocationEnd ctx.logger.Debugf("Setting border context based on bottom padding line") } else if lastRenderedLineIdx >= 0 { hctx.rowIdx = 0 hctx.lineIdx = lastRenderedLineIdx hctx.line = padLine(ctx.footerLines[hctx.lineIdx], ctx.numCols) hctx.location = tw.LocationEnd ctx.logger.Debugf("Setting border context based on last content line idx %d", hctx.lineIdx) } else if lastRenderedLineIdx == -1 { hctx.rowIdx = 0 hctx.lineIdx = -1 hctx.line = paddingLineContentForContext hctx.location = tw.LocationEnd ctx.logger.Debugf("Setting border context based on top padding line") } else { hctx.rowIdx = 0 hctx.lineIdx = -2 hctx.line = make([]string, ctx.numCols) hctx.location = tw.LocationEnd ctx.logger.Debugf("Warning: Cannot determine context for bottom border") } resp := t.buildCellContexts(ctx, mctx, hctx, colAligns, colPadding) ctx.logger.Debugf("Bottom border: Using Widths=%v", ctx.widths[tw.Row]) f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: ctx.widths[tw.Row], Current: resp.cells, Previous: resp.prevCells, Position: tw.Footer, Location: tw.LocationEnd, ColMaxWidths: t.getColMaxWidths(tw.Footer), }, Level: tw.LevelFooter, IsSubRow: false, }) } return nil } // renderHeader renders the table's header section with borders and padding. // Parameters ctx and mctx hold rendering and merge state. // Returns an error if rendering fails. func (t *Table) renderHeader(ctx *renderContext, mctx *mergeContext) error { if len(ctx.headerLines) == 0 { return nil } ctx.logger.Debug("Rendering header section") f := ctx.renderer cfg := ctx.cfg colAligns := t.buildAligns(t.config.Header) colPadding := t.buildPadding(t.config.Header.Padding) hctx := &helperContext{position: tw.Header} if cfg.Borders.Top.Enabled() && cfg.Settings.Lines.ShowTop.Enabled() { ctx.logger.Debug("Rendering table top border") nextCells := make(map[int]tw.CellContext) if len(ctx.headerLines) > 0 { for j, cell := range ctx.headerLines[0] { nextCells[j] = tw.CellContext{Data: cell, Merge: mctx.headerMerges[j]} } } f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: ctx.widths[tw.Header], Next: nextCells, Position: tw.Header, Location: tw.LocationFirst, }, Level: tw.LevelHeader, IsSubRow: false, }) } if t.config.Header.Padding.Global.Top != tw.Empty { hctx.location = tw.LocationFirst hctx.line = t.buildPaddingLineContents(t.config.Header.Padding.Global.Top, ctx.widths[tw.Header], ctx.numCols, mctx.headerMerges) if err := t.renderPadding(ctx, mctx, hctx, t.config.Header.Padding.Global.Top); err != nil { return err } } for i, line := range ctx.headerLines { hctx.rowIdx = 0 hctx.lineIdx = i hctx.line = padLine(line, ctx.numCols) hctx.location = t.determineLocation(i, len(ctx.headerLines), t.config.Header.Padding.Global.Top, t.config.Header.Padding.Global.Bottom) if t.config.Header.Callbacks.Global != nil { ctx.logger.Debug("Executing global header callback for line %d", i) t.config.Header.Callbacks.Global() } for colIdx, cb := range t.config.Header.Callbacks.PerColumn { if colIdx < ctx.numCols && cb != nil { ctx.logger.Debug("Executing per-column header callback for line %d, col %d", i, colIdx) cb() } } if err := t.renderLine(ctx, mctx, hctx, colAligns, colPadding); err != nil { return err } } if t.config.Header.Padding.Global.Bottom != tw.Empty { hctx.location = tw.LocationEnd hctx.line = t.buildPaddingLineContents(t.config.Header.Padding.Global.Bottom, ctx.widths[tw.Header], ctx.numCols, mctx.headerMerges) if err := t.renderPadding(ctx, mctx, hctx, t.config.Header.Padding.Global.Bottom); err != nil { return err } } if cfg.Settings.Lines.ShowHeaderLine.Enabled() && (len(ctx.rowLines) > 0 || len(ctx.footerLines) > 0) { ctx.logger.Debug("Rendering header separator line") resp := t.buildCellContexts(ctx, mctx, hctx, colAligns, colPadding) var nextSectionCells map[int]tw.CellContext var nextSectionWidths tw.Mapper[int, int] if len(ctx.rowLines) > 0 { nextSectionWidths = ctx.widths[tw.Row] rowColAligns := t.buildAligns(t.config.Row) rowColPadding := t.buildPadding(t.config.Row.Padding) firstRowHctx := &helperContext{ position: tw.Row, rowIdx: 0, lineIdx: 0, } if len(ctx.rowLines[0]) > 0 { firstRowHctx.line = padLine(ctx.rowLines[0][0], ctx.numCols) } else { firstRowHctx.line = make([]string, ctx.numCols) } firstRowResp := t.buildCellContexts(ctx, mctx, firstRowHctx, rowColAligns, rowColPadding) nextSectionCells = firstRowResp.cells } else if len(ctx.footerLines) > 0 { nextSectionWidths = ctx.widths[tw.Row] footerColAligns := t.buildAligns(t.config.Footer) footerColPadding := t.buildPadding(t.config.Footer.Padding) firstFooterHctx := &helperContext{ position: tw.Footer, rowIdx: 0, lineIdx: 0, } if len(ctx.footerLines) > 0 { firstFooterHctx.line = padLine(ctx.footerLines[0], ctx.numCols) } else { firstFooterHctx.line = make([]string, ctx.numCols) } firstFooterResp := t.buildCellContexts(ctx, mctx, firstFooterHctx, footerColAligns, footerColPadding) nextSectionCells = firstFooterResp.cells } else { nextSectionWidths = ctx.widths[tw.Header] nextSectionCells = nil } f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: nextSectionWidths, Current: resp.cells, Previous: resp.prevCells, Next: nextSectionCells, Position: tw.Header, Location: tw.LocationMiddle, }, Level: tw.LevelBody, IsSubRow: false, }) } return nil } // renderLine renders a single line with callbacks and normalized widths. // Parameters include ctx, mctx, hctx, aligns, and padding for rendering. // Returns an error if rendering fails. func (t *Table) renderLine(ctx *renderContext, mctx *mergeContext, hctx *helperContext, aligns map[int]tw.Align, padding map[int]tw.Padding) error { resp := t.buildCellContexts(ctx, mctx, hctx, aligns, padding) f := ctx.renderer isPaddingLine := false sectionConfig := t.config.Row switch hctx.position { case tw.Header: sectionConfig = t.config.Header isPaddingLine = (hctx.lineIdx == -1 && sectionConfig.Padding.Global.Top != tw.Empty) || (hctx.lineIdx == len(ctx.headerLines) && sectionConfig.Padding.Global.Bottom != tw.Empty) case tw.Footer: sectionConfig = t.config.Footer isPaddingLine = (hctx.lineIdx == -1 && sectionConfig.Padding.Global.Top != tw.Empty) || (hctx.lineIdx == len(ctx.footerLines) && (sectionConfig.Padding.Global.Bottom != tw.Empty || t.hasPerColumnBottomPadding())) case tw.Row: if hctx.rowIdx >= 0 && hctx.rowIdx < len(ctx.rowLines) { isPaddingLine = (hctx.lineIdx == -1 && sectionConfig.Padding.Global.Top != tw.Empty) || (hctx.lineIdx == len(ctx.rowLines[hctx.rowIdx]) && sectionConfig.Padding.Global.Bottom != tw.Empty) } } sectionWidths := ctx.widths[hctx.position] normalizedWidths := ctx.widths[tw.Row] formatting := tw.Formatting{ Row: tw.RowContext{ Widths: sectionWidths, ColMaxWidths: t.getColMaxWidths(hctx.position), Current: resp.cells, Previous: resp.prevCells, Next: resp.nextCells, Position: hctx.position, Location: hctx.location, }, Level: t.getLevel(hctx.position), IsSubRow: hctx.lineIdx > 0 || isPaddingLine, NormalizedWidths: normalizedWidths, } if hctx.position == tw.Row { formatting.HasFooter = len(ctx.footerLines) > 0 } switch hctx.position { case tw.Header: f.Header([][]string{hctx.line}, formatting) case tw.Row: f.Row(hctx.line, formatting) case tw.Footer: f.Footer([][]string{hctx.line}, formatting) } return nil } // renderPadding renders padding lines for a section. // Parameters include ctx, mctx, hctx, and padChar for padding content. // Returns an error if rendering fails. func (t *Table) renderPadding(ctx *renderContext, mctx *mergeContext, hctx *helperContext, padChar string) error { ctx.logger.Debug("Rendering padding line for %s (using char like '%s')", hctx.position, padChar) colAligns := t.buildAligns(t.config.Row) colPadding := t.buildPadding(t.config.Row.Padding) switch hctx.position { case tw.Header: colAligns = t.buildAligns(t.config.Header) colPadding = t.buildPadding(t.config.Header.Padding) case tw.Footer: colAligns = t.buildAligns(t.config.Footer) colPadding = t.buildPadding(t.config.Footer.Padding) } return t.renderLine(ctx, mctx, hctx, colAligns, colPadding) } // renderRow renders the table's row section with borders and padding. // Parameters ctx and mctx hold rendering and merge state. // Returns an error if rendering fails. func (t *Table) renderRow(ctx *renderContext, mctx *mergeContext) error { if len(ctx.rowLines) == 0 { return nil } ctx.logger.Debugf("Rendering row section (total rows: %d)", len(ctx.rowLines)) f := ctx.renderer cfg := ctx.cfg colAligns := t.buildAligns(t.config.Row) colPadding := t.buildPadding(t.config.Row.Padding) hctx := &helperContext{position: tw.Row} footerIsEmptyOrNonExistent := !t.hasFooterElements() if len(ctx.headerLines) == 0 && footerIsEmptyOrNonExistent && cfg.Borders.Top.Enabled() && cfg.Settings.Lines.ShowTop.Enabled() { ctx.logger.Debug("Rendering table top border (rows only table)") nextCells := make(map[int]tw.CellContext) if len(ctx.rowLines) > 0 && len(ctx.rowLines[0]) > 0 && len(mctx.rowMerges) > 0 { firstLine := ctx.rowLines[0][0] firstMerges := mctx.rowMerges[0] for j, cell := range padLine(firstLine, ctx.numCols) { mergeState := tw.MergeState{} if firstMerges != nil { mergeState = firstMerges[j] } nextCells[j] = tw.CellContext{Data: cell, Merge: mergeState, Width: ctx.widths[tw.Row].Get(j)} } } f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: ctx.widths[tw.Row], Next: nextCells, Position: tw.Row, Location: tw.LocationFirst, }, Level: tw.LevelHeader, IsSubRow: false, }) } for i, lines := range ctx.rowLines { rowHasTopPadding := t.config.Row.Padding.Global.Top != tw.Empty if rowHasTopPadding { hctx.rowIdx = i hctx.lineIdx = -1 if i == 0 && len(ctx.headerLines) == 0 { hctx.location = tw.LocationFirst } else { hctx.location = tw.LocationMiddle } hctx.line = t.buildPaddingLineContents(t.config.Row.Padding.Global.Top, ctx.widths[tw.Row], ctx.numCols, mctx.rowMerges[i]) ctx.logger.Debug("Calling renderPadding for Row Top Padding (row %d): %v (loc: %v)", i, hctx.line, hctx.location) if err := t.renderPadding(ctx, mctx, hctx, t.config.Row.Padding.Global.Top); err != nil { return err } } footerExists := t.hasFooterElements() rowHasBottomPadding := t.config.Row.Padding.Global.Bottom != tw.Empty isLastRow := i == len(ctx.rowLines)-1 for j, visualLineData := range lines { hctx.rowIdx = i hctx.lineIdx = j hctx.line = padLine(visualLineData, ctx.numCols) if j > 0 { visualLineHasActualContent := false for kCellIdx, cellContentInVisualLine := range hctx.line { if t.Trimmer(cellContentInVisualLine) != "" { visualLineHasActualContent = true ctx.logger.Debug("Visual line [%d][%d] has content in cell %d: '%s'. Not skipping.", i, j, kCellIdx, cellContentInVisualLine) break } } if !visualLineHasActualContent { ctx.logger.Debug("Skipping visual line [%d][%d] as it's entirely blank after trimming. Line: %q", i, j, hctx.line) continue } } isFirstRow := i == 0 isLastLineOfRow := j == len(lines)-1 if isFirstRow && j == 0 && !rowHasTopPadding && len(ctx.headerLines) == 0 { hctx.location = tw.LocationFirst } else if isLastRow && isLastLineOfRow && !rowHasBottomPadding && !footerExists { hctx.location = tw.LocationEnd } else { hctx.location = tw.LocationMiddle } ctx.logger.Debugf("Rendering row %d line %d with location %v. Content: %q", i, j, hctx.location, hctx.line) if err := t.renderLine(ctx, mctx, hctx, colAligns, colPadding); err != nil { return err } } if rowHasBottomPadding { hctx.rowIdx = i hctx.lineIdx = len(lines) if isLastRow && !footerExists { hctx.location = tw.LocationEnd } else { hctx.location = tw.LocationMiddle } hctx.line = t.buildPaddingLineContents(t.config.Row.Padding.Global.Bottom, ctx.widths[tw.Row], ctx.numCols, mctx.rowMerges[i]) ctx.logger.Debug("Calling renderPadding for Row Bottom Padding (row %d): %v (loc: %v)", i, hctx.line, hctx.location) if err := t.renderPadding(ctx, mctx, hctx, t.config.Row.Padding.Global.Bottom); err != nil { return err } } if cfg.Settings.Separators.BetweenRows.Enabled() && !isLastRow { ctx.logger.Debug("Rendering between-rows separator after logical row %d", i) respCurrent := t.buildCellContexts(ctx, mctx, hctx, colAligns, colPadding) var nextCellsForSeparator map[int]tw.CellContext = nil nextRowIdx := i + 1 if nextRowIdx < len(ctx.rowLines) && nextRowIdx < len(mctx.rowMerges) { hctxNext := &helperContext{position: tw.Row, rowIdx: nextRowIdx, location: tw.LocationMiddle} nextRowActualLines := ctx.rowLines[nextRowIdx] nextRowMerges := mctx.rowMerges[nextRowIdx] if t.config.Row.Padding.Global.Top != tw.Empty { hctxNext.lineIdx = -1 hctxNext.line = t.buildPaddingLineContents(t.config.Row.Padding.Global.Top, ctx.widths[tw.Row], ctx.numCols, nextRowMerges) } else if len(nextRowActualLines) > 0 { hctxNext.lineIdx = 0 hctxNext.line = padLine(nextRowActualLines[0], ctx.numCols) } else { hctxNext.lineIdx = 0 hctxNext.line = make([]string, ctx.numCols) } respNext := t.buildCellContexts(ctx, mctx, hctxNext, colAligns, colPadding) nextCellsForSeparator = respNext.cells } else { ctx.logger.Debug("Separator context: No next logical row for separator after row %d.", i) } f.Line(tw.Formatting{ Row: tw.RowContext{ Widths: ctx.widths[tw.Row], Current: respCurrent.cells, Previous: respCurrent.prevCells, Next: nextCellsForSeparator, Position: tw.Row, Location: tw.LocationMiddle, ColMaxWidths: t.getColMaxWidths(tw.Row), }, Level: tw.LevelBody, IsSubRow: false, HasFooter: footerExists, }) } } return nil } tablewriter-1.0.9/tablewriter_test.go000066400000000000000000000604071504170215200177630ustar00rootroot00000000000000package tablewriter import ( "bytes" "database/sql" "fmt" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/tw" "os" "reflect" "sync" "testing" ) // TestBuildPaddingLineContents tests the buildPaddingLineContents function with various configurations. // It verifies padding line construction with different widths and merge states. func TestBuildPaddingLineContents(t *testing.T) { table := NewTable(os.Stdout) widths := tw.NewMapper[int, int]() widths.Set(0, 5) widths.Set(1, 10) merges := map[int]tw.MergeState{ 1: {Horizontal: tw.MergeStateOption{Present: true, Start: false}}, } t.Run("Basic Padding", func(t *testing.T) { line := table.buildPaddingLineContents("-", widths, 2, nil) expected := []string{"-----", "----------"} if !reflect.DeepEqual(line, expected) { t.Errorf("Expected %v, got %v", expected, line) } }) t.Run("With Merge", func(t *testing.T) { line := table.buildPaddingLineContents("-", widths, 2, merges) expected := []string{"-----", ""} if !reflect.DeepEqual(line, expected) { t.Errorf("Expected %v, got %v", expected, line) } }) t.Run("Zero Width", func(t *testing.T) { widths.Set(0, 0) line := table.buildPaddingLineContents("-", widths, 2, nil) expected := []string{"", "----------"} if !reflect.DeepEqual(line, expected) { t.Errorf("Expected %v, got %v", expected, line) } }) } // TestCalculateContentMaxWidth tests the calculateContentMaxWidth function in batch and streaming modes. // It verifies width calculations with various constraints and padding settings. func TestCalculateContentMaxWidth(t *testing.T) { table := NewTable(os.Stdout) config := tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapTruncate, Alignment: tw.AlignLeft, AutoFormat: tw.Off, }, ColMaxWidths: tw.CellWidth{Global: 10}, Padding: tw.CellPadding{ Global: tw.Padding{Left: " ", Right: " "}, }, } t.Run("Batch Mode with MaxWidth", func(t *testing.T) { got := table.calculateContentMaxWidth(0, config, 1, 1, false) if got != 8 { // 10 - 1 (left) - 1 (right) t.Errorf("Expected width 8, got %d", got) } }) t.Run("Streaming Mode", func(t *testing.T) { table.streamWidths = map[int]int{0: 12} table.config.Stream.Enable = true table.hasPrinted = true got := table.calculateContentMaxWidth(0, config, 1, 1, true) if got != 10 { // 12 - 1 (left) - 1 (right) t.Errorf("Expected width 10, got %d", got) } }) t.Run("No Constraint in Batch", func(t *testing.T) { config.ColMaxWidths.Global = 0 got := table.calculateContentMaxWidth(0, config, 1, 1, false) if got != 0 { t.Errorf("Expected width 0, got %d", got) } }) } // TestCallStringer tests the convertToStringer function with caching enabled and disabled. // It verifies stringer invocation and cache behavior for custom types. func TestCallStringer(t *testing.T) { table := &Table{ logger: ll.New("test"), stringer: func(s interface{}) []string { return []string{fmt.Sprintf("%v", s)} }, stringerCache: map[reflect.Type]reflect.Value{}, stringerCacheEnabled: true, } input := struct{ Name string }{Name: "test"} cells, err := table.convertToStringer(input) if err != nil { t.Errorf("convertToStringer failed: %v", err) } if len(cells) != 1 || cells[0] != "{test}" { t.Errorf("convertToStringer returned unexpected cells: %v", cells) } // Test cache hit cells, err = table.convertToStringer(input) if err != nil { t.Errorf("convertToStringer failed on cache hit: %v", err) } if len(cells) != 1 || cells[0] != "{test}" { t.Errorf("convertToStringer returned unexpected cells on cache hit: %v", cells) } // Test disabled cache table.stringerCacheEnabled = false cells, err = table.convertToStringer(input) if err != nil { t.Errorf("convertToStringer failed without cache: %v", err) } if len(cells) != 1 || cells[0] != "{test}" { t.Errorf("convertToStringer returned unexpected cells without cache: %v", cells) } } // TestCallStringerConcurrent tests the convertToStringer function under concurrent access. // It verifies thread-safety of the stringer cache with multiple goroutines. func TestCallStringerConcurrent(t *testing.T) { table := &Table{ logger: ll.New("test"), stringer: func(s interface{}) []string { return []string{fmt.Sprintf("%v", s)} }, stringerCacheEnabled: true, stringerCache: map[reflect.Type]reflect.Value{}, } var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func(i int) { defer wg.Done() input := struct{ ID int }{ID: i} cells, err := table.convertToStringer(input) if err != nil { t.Errorf("convertToStringer failed for ID %d: %v", i, err) } expected := fmt.Sprintf("{%d}", i) if len(cells) != 1 || cells[0] != expected { t.Errorf("convertToStringer returned unexpected cells for ID %d: got %v, want [%s]", i, cells, expected) } }(i) } wg.Wait() } // TestCallbacks tests the CellCallbacks functionality with the hybrid configuration approach. // It verifies callbacks are triggered during rendering using WithConfig, Configure, and ConfigBuilder. func TestCallbacks(t *testing.T) { tests := []struct { name string setup func(*Table) // How to configure the table expectedGlob int // Expected global callback count expectedCol0 int // Expected column 0 callback count }{ // Test case: Using WithConfig Option { name: "WithConfig", setup: func(t *Table) { t.Header([]string{"Name", "Email", "Age"}) t.Append([]string{"Alice", "alice@example.com", "25"}) }, expectedGlob: 1, // One header line expectedCol0: 1, // One callback for column 0 }, // Test case: Using Configure method { name: "Configure", setup: func(t *Table) { t.Configure(func(cfg *Config) { cfg.Header.Callbacks = tw.CellCallbacks{ Global: t.config.Header.Callbacks.Global, // Preserve from base PerColumn: []func(){ t.config.Header.Callbacks.PerColumn[0], // Preserve column 0 nil, nil, }, } }) t.Header([]string{"Name", "Email", "Age"}) t.Append([]string{"Bob", "bob@example.com", "30"}) }, expectedGlob: 1, expectedCol0: 1, }, // Test case: Using ConfigBuilder { name: "ConfigBuilder", setup: func(t *Table) { config := NewConfigBuilder(). Header(). Build(). Build() t.config = mergeConfig(t.config, config) // Apply builder config t.Header([]string{"Name", "Email", "Age"}) t.Append([]string{"Charlie", "charlie@example.com", "35"}) }, expectedGlob: 1, expectedCol0: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var buf bytes.Buffer globalCount := 0 col0Count := 0 // Base configuration with callbacks baseConfig := Config{ Header: tw.CellConfig{ Callbacks: tw.CellCallbacks{ Global: func() { globalCount++ }, PerColumn: []func(){ func() { col0Count++ }, // Callback for column 0 nil, nil, }, }, }, } // Create table with base config table := NewTable(&buf, WithConfig(baseConfig)) // Apply test-specific setup tt.setup(table) // Render to trigger callbacks table.Render() // Verify callback counts if globalCount != tt.expectedGlob { t.Errorf("%s: Expected global callback to run %d time(s), got %d", tt.name, tt.expectedGlob, globalCount) } if col0Count != tt.expectedCol0 { t.Errorf("%s: Expected column 0 callback to run %d time(s), got %d", tt.name, tt.expectedCol0, col0Count) } }) } } // TestConvertToString tests the convertToString function with various input types. // It verifies correct string conversion for nil, strings, SQL null types, and errors. func TestConvertToString(t *testing.T) { table := &Table{logger: ll.New("test")} tests := []struct { input interface{} expected string }{ {nil, ""}, {"test", "test"}, {[]byte("bytes"), "bytes"}, {fmt.Errorf("err"), "err"}, {sql.NullString{String: "valid", Valid: true}, "valid"}, {sql.NullString{Valid: false}, ""}, // Add more cases } for _, tt := range tests { result := table.convertToString(tt.input) if tt.expected != result { t.Errorf("ConvertToString: expected %v, got '%v'", tt.expected, result) } } } // TestEnsureStreamWidthsCalculated tests the ensureStreamWidthsCalculated function. // It verifies stream width initialization in streaming mode with various inputs. func TestEnsureStreamWidthsCalculated(t *testing.T) { table := NewTable(os.Stdout, WithStreaming(tw.StreamConfig{Enable: true})) sampleData := []string{"A", "B"} config := tw.CellConfig{} t.Run("Already Initialized", func(t *testing.T) { table.streamWidths = tw.NewMapper[int, int]().Set(0, 5).Set(1, 5) table.streamNumCols = 2 err := table.ensureStreamWidthsCalculated(sampleData, config) if err != nil { t.Errorf("Expected nil, got %v", err) } }) t.Run("Initialize New", func(t *testing.T) { table.streamWidths = nil table.streamNumCols = 0 err := table.ensureStreamWidthsCalculated(sampleData, config) if err != nil { t.Errorf("Expected nil, got %v", err) } if table.streamNumCols != 2 { t.Errorf("Expected streamNumCols=2, got %d", table.streamNumCols) } if table.streamWidths.Len() < 2 { t.Errorf("Expected at least 2 widths, got %d", table.streamWidths.Len()) } }) t.Run("Zero Columns", func(t *testing.T) { table.streamWidths = nil table.streamNumCols = 0 err := table.ensureStreamWidthsCalculated([]string{}, config) if err == nil || err.Error() != "failed to determine column count for streaming" { t.Errorf("Expected error for zero columns, got %v", err) } }) } // Helper to get a fresh default CellConfig for a section. // This uses the defaultConfig() function from the tablewriter package. func getTestSectionDefaultConfig(section string) tw.CellConfig { fullDefaultCfg := defaultConfig() switch section { case "header": return fullDefaultCfg.Header case "row": return fullDefaultCfg.Row case "footer": return fullDefaultCfg.Footer default: return fullDefaultCfg.Row } } // TestMergeCellConfig comprehensively tests the mergeCellConfig function. func TestMergeCellConfig(t *testing.T) { tests := []struct { name string baseConfig func() tw.CellConfig inputConfig tw.CellConfig expectedConfig func() tw.CellConfig }{ { name: "EmptyInput_OnRowDefaultBase", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{}, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") // src.AutoFormat is 0 (Pending) from empty CellConfig. // mergeCellConfig assigns src.AutoFormat to dst.AutoFormat. cfg.Formatting.AutoFormat = tw.Pending // Should be 0 return cfg }, }, { name: "OverrideMergeModeNone_OnRowDefaultBase", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeNone}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Formatting.MergeMode = tw.MergeNone cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "OverrideMergeModeVertical_OnRowDefaultBase", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeVertical}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Formatting.MergeMode = tw.MergeVertical cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "OverrideMergeModeHorizontal_OnRowDefaultBase", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Formatting.MergeMode = tw.MergeHorizontal cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "OverrideMergeModeBoth_OnRowDefaultBase", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeBoth}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Formatting.MergeMode = tw.MergeBoth cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "OverrideMergeModeHierarchical_OnRowDefaultBase", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Formatting.MergeMode = tw.MergeHierarchical cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "ConfigBuilderOutput_MergingIntoRowDefault", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, // Base AutoFormat = tw.Off (-1) inputConfig: NewConfigBuilder(). WithHeaderAlignment(tw.AlignCenter). WithHeaderMergeMode(tw.MergeHorizontal). Build().Header, // Src AutoFormat = tw.On (1) from defaultConfig().Header expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Alignment.Global = tw.AlignCenter cfg.Formatting.AutoWrap = tw.WrapTruncate // from defaultConfig().Header cfg.Formatting.AutoFormat = tw.On // from src (Builder's Header) cfg.Formatting.MergeMode = tw.MergeHorizontal // Padding.Global is same in default row and header, so it's effectively overwritten by itself. return cfg }, }, { name: "Row_EmptyInput_OnRowBase", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{}, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "Row_OverrideAlignment_OnRowBase", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ Formatting: tw.CellFormatting{Alignment: tw.AlignCenter}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Formatting.Alignment = tw.AlignCenter cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "Row_OverrideColumnAligns_OnRowBase_WithSkip", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ ColumnAligns: []tw.Align{tw.AlignRight, tw.Skip, tw.AlignLeft}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.ColumnAligns = []tw.Align{tw.AlignRight, tw.Empty, tw.AlignLeft} cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "Row_OverrideAutoWrap_OnRowBase", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ Formatting: tw.CellFormatting{AutoWrap: tw.WrapTruncate}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Formatting.AutoWrap = tw.WrapTruncate cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "Row_OverrideAutoFormat_InputOff_BaseOff", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, // Base AutoFormat = tw.Off (-1) inputConfig: tw.CellConfig{ Formatting: tw.CellFormatting{AutoFormat: tw.Off}, // Src AutoFormat = tw.Off (-1) }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Formatting.AutoFormat = tw.Off // Expected -1 return cfg }, }, { name: "Row_OverrideAutoFormat_InputOn_BaseOff", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, // Base AutoFormat = tw.Off (-1) inputConfig: tw.CellConfig{ Formatting: tw.CellFormatting{AutoFormat: tw.On}, // Src AutoFormat = tw.On (1) }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Formatting.AutoFormat = tw.On // Expected 1 return cfg }, }, { name: "Header_ConfigBuilderOutput_MergingIntoHeaderDefault", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("header") }, // Base AutoFormat = tw.On (1) inputConfig: NewConfigBuilder(). WithHeaderAlignment(tw.AlignCenter). WithHeaderMergeMode(tw.MergeHorizontal). Build().Header, // Src AutoFormat = tw.On (1) expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("header") cfg.Formatting.MergeMode = tw.MergeHorizontal cfg.Formatting.AutoFormat = tw.On return cfg }, }, { name: "OverrideColMaxWidthGlobal_OnRowDefault", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ ColMaxWidths: tw.CellWidth{Global: 50}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.ColMaxWidths.Global = 50 cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "MergeColMaxWidthPerColumn_NewEntries_OnRowDefault", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ ColMaxWidths: tw.CellWidth{PerColumn: map[int]int{0: 10, 2: 20}}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.ColMaxWidths.PerColumn = map[int]int{0: 10, 2: 20} cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "MergeColMaxWidthPerColumn_OverwriteAndAdd_OnExisting", baseConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.ColMaxWidths.PerColumn = map[int]int{0: 5, 1: 15} return cfg }, inputConfig: tw.CellConfig{ ColMaxWidths: tw.CellWidth{PerColumn: map[int]int{0: 10, 2: 20, 1: 0}}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.ColMaxWidths.PerColumn = map[int]int{0: 10, 1: 15, 2: 20} cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "MergePaddingPerColumn_NewEntries_OnRowDefault", baseConfig: func() tw.CellConfig { return getTestSectionDefaultConfig("row") }, inputConfig: tw.CellConfig{ Padding: tw.CellPadding{PerColumn: []tw.Padding{ {Left: "L0", Right: "R0"}, {}, {Left: "L2", Right: "R2"}, }}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Padding.PerColumn = []tw.Padding{ {Left: "L0", Right: "R0"}, {}, {Left: "L2", Right: "R2"}, } cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "MergePaddingPerColumn_OverwriteExtendAndPreserve_OnExisting", baseConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Padding.PerColumn = []tw.Padding{ {Left: "BASE_L0"}, {Left: "BASE_L1"}, {Left: "BASE_L2"}, } return cfg }, inputConfig: tw.CellConfig{ Padding: tw.CellPadding{PerColumn: []tw.Padding{ {Left: "SRC_L0"}, {}, }}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Padding.PerColumn = []tw.Padding{ {Left: "SRC_L0"}, {Left: "BASE_L1"}, {Left: "BASE_L2"}, } cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, { name: "MergeColumnAligns_SrcShorterThanDst_WithSkipAndEmpty", baseConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.ColumnAligns = []tw.Align{tw.AlignCenter, tw.AlignRight, tw.AlignCenter} return cfg }, inputConfig: tw.CellConfig{ ColumnAligns: []tw.Align{tw.AlignLeft, tw.Skip}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.ColumnAligns = []tw.Align{tw.AlignLeft, tw.AlignRight, tw.AlignCenter} cfg.Formatting.AutoFormat = tw.Pending // src.AutoFormat is 0 return cfg }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { baseForTest := tt.baseConfig() expected := tt.expectedConfig() got := mergeCellConfig(baseForTest, tt.inputConfig) // Use a more verbose comparison for Formatting if DeepEqual fails to pinpoint AutoFormat if !reflect.DeepEqual(got.Formatting, expected.Formatting) { if got.Formatting.Alignment != expected.Formatting.Alignment || got.Formatting.AutoWrap != expected.Formatting.AutoWrap || got.Formatting.MergeMode != expected.Formatting.MergeMode || got.Formatting.AutoFormat != expected.Formatting.AutoFormat { // Key comparison t.Errorf("Formatting mismatch:\nexpected: Alignment:%v, AutoWrap:%d, MergeMode:%d, AutoFormat:%d (%s)\ngot: Alignment:%v, AutoWrap:%d, MergeMode:%d, AutoFormat:%d (%s)", expected.Formatting.Alignment, expected.Formatting.AutoWrap, expected.Formatting.MergeMode, expected.Formatting.AutoFormat, expected.Formatting.AutoFormat.String(), got.Formatting.Alignment, got.Formatting.AutoWrap, got.Formatting.MergeMode, got.Formatting.AutoFormat, got.Formatting.AutoFormat.String()) } else { // Fallback if the above doesn't catch it (shouldn't happen for simple fields) t.Errorf("Formatting mismatch (DeepEqual failed):\nexpected: %+v\ngot: %+v", expected.Formatting, got.Formatting) } } if !reflect.DeepEqual(got.Padding.Global, expected.Padding.Global) { t.Errorf("Padding.Global mismatch\nexpected: %+v\ngot: %+v", expected.Padding.Global, got.Padding.Global) } if !reflect.DeepEqual(got.Padding.PerColumn, expected.Padding.PerColumn) { t.Errorf("Padding.PerColumn mismatch\nexpected: %#v\ngot: %#v", expected.Padding.PerColumn, got.Padding.PerColumn) } if !reflect.DeepEqual(got.ColMaxWidths.Global, expected.ColMaxWidths.Global) { t.Errorf("ColMaxWidths.Global mismatch\nexpected: %d\ngot: %d", expected.ColMaxWidths.Global, got.ColMaxWidths.Global) } if !reflect.DeepEqual(got.ColMaxWidths.PerColumn, expected.ColMaxWidths.PerColumn) { t.Errorf("ColMaxWidths.PerColumn mismatch\nexpected: %#v\ngot: %#v", expected.ColMaxWidths.PerColumn, got.ColMaxWidths.PerColumn) } if !reflect.DeepEqual(got.ColumnAligns, expected.ColumnAligns) { t.Errorf("ColumnAligns mismatch\nexpected: %#v\ngot: %#v", expected.ColumnAligns, got.ColumnAligns) } if (got.Callbacks.Global == nil) != (expected.Callbacks.Global == nil) { t.Errorf("Callbacks.Global nilness mismatch\nexpected nil: %t, got nil: %t", expected.Callbacks.Global == nil, got.Callbacks.Global == nil) } if len(got.Callbacks.PerColumn) != len(expected.Callbacks.PerColumn) { t.Errorf("Callbacks.PerColumn length mismatch\nexpected: %d, got: %d", len(expected.Callbacks.PerColumn), len(got.Callbacks.PerColumn)) } else { for i := range got.Callbacks.PerColumn { if (got.Callbacks.PerColumn[i] == nil) != (expected.Callbacks.PerColumn[i] == nil) { t.Errorf("Callbacks.PerColumn[%d] nilness mismatch\nexpected nil: %t, got nil: %t", i, expected.Callbacks.PerColumn[i] == nil, got.Callbacks.PerColumn[i] == nil) break } } } if (got.Filter.Global == nil) != (expected.Filter.Global == nil) { t.Errorf("Filter.Global nilness mismatch\nexpected nil: %t, got nil: %t", expected.Filter.Global == nil, got.Filter.Global == nil) } if len(got.Filter.PerColumn) != len(expected.Filter.PerColumn) { t.Errorf("Filter.PerColumn length mismatch\nexpected: %d, got: %d", len(expected.Filter.PerColumn), len(got.Filter.PerColumn)) } else { for i := range got.Filter.PerColumn { if (got.Filter.PerColumn[i] == nil) != (expected.Filter.PerColumn[i] == nil) { t.Errorf("Filter.PerColumn[%d] nilness mismatch\nexpected nil: %t, got nil: %t", i, expected.Filter.PerColumn[i] == nil, got.Filter.PerColumn[i] == nil) break } } } }) } } tablewriter-1.0.9/tests/000077500000000000000000000000001504170215200152045ustar00rootroot00000000000000tablewriter-1.0.9/tests/basic_test.go000066400000000000000000000751301504170215200176610ustar00rootroot00000000000000package tests import ( "bytes" "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "testing" ) func TestBasicTableDefault(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` ┌───────┬─────┬──────────┐ │ NAME │ AGE │ CITY │ ├───────┼─────┼──────────┤ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ └───────┴─────┴──────────┘ ` debug := visualCheck(t, "TestBasicTableDefault", buf.String(), expected) if !debug { t.Error(table.Debug()) } } func TestBasicTableDefaultBorder(t *testing.T) { var buf bytes.Buffer t.Run("all-off", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Borders: tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off}, })), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` NAME │ AGE │ CITY ───────┼─────┼────────── Alice │ 25 │ New York Bob │ 30 │ Boston ` visualCheck(t, "TestBasicTableDefaultBorder-top-off", buf.String(), expected) }) t.Run("top-on", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Borders: tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.On, Bottom: tw.Off}, })), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` ───────┬─────┬────────── NAME │ AGE │ CITY ───────┼─────┼────────── Alice │ 25 │ New York Bob │ 30 │ Boston ` visualCheck(t, "TestBasicTableDefaultBorder-top-on", buf.String(), expected) }) t.Run("mix", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Borders: tw.Border{Left: tw.Off, Right: tw.On, Top: tw.On, Bottom: tw.On}, })), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` ───────┬─────┬──────────┐ NAME │ AGE │ CITY │ ───────┼─────┼──────────┤ Alice │ 25 │ New York │ Bob │ 30 │ Boston │ ───────┴─────┴──────────┘ ` visualCheck(t, "TestBasicTableDefaultBorder-mix", buf.String(), expected) }) } func TestUnicodeWithoutHeader(t *testing.T) { data := [][]string{ {"Regular", "regular line", "1"}, {"Thick", "particularly thick line", "2"}, {"Double", "double line", "3"}, } var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.Off, Bottom: tw.Off}, })), ) table.Header([]string{"Name", "Age", "City"}) table.Bulk(data) table.Render() expected := ` │ NAME │ AGE │ CITY │ ├─────────┼─────────────────────────┼──────┤ │ Regular │ regular line │ 1 │ │ Thick │ particularly thick line │ 2 │ │ Double │ double line │ 3 │ ` visualCheck(t, "UnicodeWithoutHeader", buf.String(), expected) } func TestBasicTableASCII(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleASCII), })), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` +-------+-----+----------+ | NAME | AGE | CITY | +-------+-----+----------+ | Alice | 25 | New York | | Bob | 30 | Boston | +-------+-----+----------+ ` visualCheck(t, "BasicTableASCII", buf.String(), expected) } func TestBasicTableUnicodeRounded(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleRounded), })), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` ╭───────┬─────┬──────────╮ │ NAME │ AGE │ CITY │ ├───────┼─────┼──────────┤ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ ╰───────┴─────┴──────────╯ ` visualCheck(t, "BasicTableUnicodeRounded", buf.String(), expected) } func TestBasicTableUnicodeDouble(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleDouble), })), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` ╔═══════╦═════╦══════════╗ ║ NAME ║ AGE ║ CITY ║ ╠═══════╬═════╬══════════╣ ║ Alice ║ 25 ║ New York ║ ║ Bob ║ 30 ║ Boston ║ ╚═══════╩═════╩══════════╝ ` visualCheck(t, "TableUnicodeDouble", buf.String(), expected) } func TestSeparator(t *testing.T) { data := [][]string{ {"Regular", "regular line", "1"}, {"Thick", "particularly thick line", "2"}, {"Double", "double line", "3"}, } t.Run("horizontal - enabled", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenColumns: tw.On, BetweenRows: tw.On, }, Lines: tw.Lines{ ShowHeaderLine: tw.On, }, }, })), ) table.Header([]string{"Name", "Age", "City"}) table.Bulk(data) table.Render() expected := ` ┌─────────┬─────────────────────────┬──────┐ │ NAME │ AGE │ CITY │ ├─────────┼─────────────────────────┼──────┤ │ Regular │ regular line │ 1 │ ├─────────┼─────────────────────────┼──────┤ │ Thick │ particularly thick line │ 2 │ ├─────────┼─────────────────────────┼──────┤ │ Double │ double line │ 3 │ └─────────┴─────────────────────────┴──────┘ ` visualCheck(t, "HorizontalEnabled", buf.String(), expected) }) t.Run("horizontal - disabled", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenColumns: tw.On, BetweenRows: tw.Off, }, Lines: tw.Lines{ ShowHeaderLine: tw.On, }, }, })), ) table.Header([]string{"Name", "Age", "City"}) table.Bulk(data) table.Render() expected := ` ┌─────────┬─────────────────────────┬──────┐ │ NAME │ AGE │ CITY │ ├─────────┼─────────────────────────┼──────┤ │ Regular │ regular line │ 1 │ │ Thick │ particularly thick line │ 2 │ │ Double │ double line │ 3 │ └─────────┴─────────────────────────┴──────┘ ` visualCheck(t, "Separator", buf.String(), expected) }) t.Run("vertical - enabled", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenColumns: tw.On, BetweenRows: tw.Off, }, Lines: tw.Lines{ ShowHeaderLine: tw.On, }, }, })), ) table.Header([]string{"Name", "Age", "City"}) table.Bulk(data) table.Render() expected := ` ┌─────────┬─────────────────────────┬──────┐ │ NAME │ AGE │ CITY │ ├─────────┼─────────────────────────┼──────┤ │ Regular │ regular line │ 1 │ │ Thick │ particularly thick line │ 2 │ │ Double │ double line │ 3 │ └─────────┴─────────────────────────┴──────┘ ` visualCheck(t, "VerticalEnabled", buf.String(), expected) }) t.Run("vertical - disabled", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenColumns: tw.Off, BetweenRows: tw.Off, }, Lines: tw.Lines{ ShowHeaderLine: tw.On, }, }, })), ) table.Header([]string{"Name", "Age", "City"}) table.Bulk(data) table.Render() expected := ` ┌────────────────────────────────────────┐ │ NAME AGE CITY │ ├────────────────────────────────────────┤ │ Regular regular line 1 │ │ Thick particularly thick line 2 │ │ Double double line 3 │ └────────────────────────────────────────┘ ` visualCheck(t, "VerticalDisabled", buf.String(), expected) }) } func TestLongHeaders(t *testing.T) { var buf bytes.Buffer t.Run("long-headers", func(t *testing.T) { c := tablewriter.Config{ Header: tw.CellConfig{Formatting: tw.CellFormatting{ AutoWrap: tw.WrapTruncate, }, ColMaxWidths: tw.CellWidth{Global: 30}, }, } buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c), tablewriter.WithDebug(true)) table.Header([]string{"Name", "Age", "This is a very long header, let see if this will be properly wrapped"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` ┌───────┬─────┬─────────────────────────────┐ │ NAME │ AGE │ THIS IS A VERY LONG HEADER… │ ├───────┼─────┼─────────────────────────────┤ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ └───────┴─────┴─────────────────────────────┘ ` if !visualCheck(t, "TestLongHeaders", buf.String(), expected) { t.Log(table.Debug()) } }) t.Run("long-headers-no-truncate", func(t *testing.T) { buf.Reset() c := tablewriter.Config{ Header: tw.CellConfig{Formatting: tw.CellFormatting{ AutoWrap: tw.WrapNormal, }, ColMaxWidths: tw.CellWidth{Global: 30}}, } table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c)) table.Header([]string{"Name", "Age", "This is a very long header, let see if this will be properly wrapped"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` ┌───────┬─────┬────────────────────────────┐ │ NAME │ AGE │ THIS IS A VERY LONG HEADER │ │ │ │ , LET SEE IF THIS WILL BE │ │ │ │ PROPERLY WRAPPED │ ├───────┼─────┼────────────────────────────┤ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ └───────┴─────┴────────────────────────────┘ ` if !visualCheck(t, "LongHeaders", buf.String(), expected) { t.Log(table.Debug()) } }) } func TestLongValues(t *testing.T) { data := [][]string{ {"1", "Learn East has computers with adapted keyboards with enlarged print etc", "Some Data", "Another Data"}, {"2", "Instead of lining up the letters all", "the way across, he splits the keyboard in two", "Like most ergonomic keyboards"}, {"3", "Nice", "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's \n" + "standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen bok", "Like most ergonomic keyboards"}, } c := tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoFormat: tw.On, }, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, ColMaxWidths: tw.CellWidth{Global: 30}, }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapNormal, }, Alignment: tw.CellAlignment{Global: tw.AlignLeft}, ColMaxWidths: tw.CellWidth{Global: 30}, }, Footer: tw.CellConfig{ Alignment: tw.CellAlignment{ Global: tw.AlignRight, PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.Skip, tw.AlignLeft}, }, }, } var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c)) table.Header([]string{"No", "Comments", "Another", ""}) table.Footer([]string{"", "", "---------->", "<---------"}) table.Bulk(data) table.Render() expected := ` ┌────┬─────────────────────────────┬──────────────────────────────┬─────────────────────┐ │ NO │ COMMENTS │ ANOTHER │ │ ├────┼─────────────────────────────┼──────────────────────────────┼─────────────────────┤ │ 1 │ Learn East has computers │ Some Data │ Another Data │ │ │ with adapted keyboards with │ │ │ │ │ enlarged print etc │ │ │ │ 2 │ Instead of lining up the │ the way across, he splits │ Like most ergonomic │ │ │ letters all │ the keyboard in two │ keyboards │ │ 3 │ Nice │ Lorem Ipsum is simply │ Like most ergonomic │ │ │ │ dummy text of the printing │ keyboards │ │ │ │ and typesetting industry. │ │ │ │ │ Lorem Ipsum has been the │ │ │ │ │ industry's │ │ │ │ │ standard dummy text ever │ │ │ │ │ since the 1500s, when an │ │ │ │ │ unknown printer took a │ │ │ │ │ galley of type and scrambled │ │ │ │ │ it to make a type specimen │ │ │ │ │ bok │ │ ├────┼─────────────────────────────┼──────────────────────────────┼─────────────────────┤ │ │ │ ----------> │ <--------- │ └────┴─────────────────────────────┴──────────────────────────────┴─────────────────────┘ ` if !visualCheck(t, "LongValues", buf.String(), expected) { t.Error(table.Debug()) } } func TestWrapping(t *testing.T) { data := [][]string{ {"1", "https://github.com/olekukonko/ruta", "routing websocket"}, {"2", "https://github.com/olekukonko/error", "better error"}, {"3", "https://github.com/olekukonko/tablewriter", "terminal\ntable"}, } c := tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoFormat: tw.On, }, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapBreak, }, Alignment: tw.CellAlignment{Global: tw.AlignLeft}, ColMaxWidths: tw.CellWidth{Global: 33}, }, Footer: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignRight}, ColMaxWidths: tw.CellWidth{Global: 30}, }, } var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c)) table.Header([]string{"No", "Package", "Comments"}) table.Bulk(data) table.Render() expected := ` ┌────┬─────────────────────────────────┬───────────────────┐ │ NO │ PACKAGE │ COMMENTS │ ├────┼─────────────────────────────────┼───────────────────┤ │ 1 │ https://github.com/olekukonko/↩ │ routing websocket │ │ │ ruta │ │ │ 2 │ https://github.com/olekukonko/↩ │ better error │ │ │ error │ │ │ 3 │ https://github.com/olekukonko/↩ │ terminal │ │ │ tablewriter │ table │ └────┴─────────────────────────────────┴───────────────────┘ ` visualCheck(t, "Wrapping", buf.String(), expected) } func TestTableWithCustomPadding(t *testing.T) { data := [][]string{ {"Regular", "regular line", "1"}, {"Thick", "particularly thick line", "2"}, {"Double", "double line", "3"}, } c := tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoFormat: tw.On, }, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, Padding: tw.CellPadding{ Global: tw.Padding{Left: " ", Right: " ", Top: "^", Bottom: "^"}, }, }, Row: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignCenter}, Padding: tw.CellPadding{ Global: tw.Padding{Left: "L", Right: "R", Top: "T", Bottom: "B"}, }, }, Footer: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoFormat: tw.On, }, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, Padding: tw.CellPadding{ Global: tw.Padding{Left: "*", Right: "*", Top: "", Bottom: ""}, }, }, } var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c)) table.Header([]string{"Name", "Age", "City"}) table.Bulk(data) table.Render() expected := ` ┌─────────┬─────────────────────────┬──────┐ │ ^^^^^^^ │ ^^^^^^^^^^^^^^^^^^^^^^^ │ ^^^^ │ │ NAME │ AGE │ CITY │ │ ^^^^^^^ │ ^^^^^^^^^^^^^^^^^^^^^^^ │ ^^^^ │ ├─────────┼─────────────────────────┼──────┤ │LTTTTTTTR│LTTTTTTTTTTTTTTTTTTTTTTTR│LTTTTR│ │LRegularR│LLLLLLregular lineRRRRRRR│LL1RRR│ │LBBBBBBBR│LBBBBBBBBBBBBBBBBBBBBBBBR│LBBBBR│ │LTTTTTTTR│LTTTTTTTTTTTTTTTTTTTTTTTR│LTTTTR│ │LLThickRR│Lparticularly thick lineR│LL2RRR│ │LBBBBBBBR│LBBBBBBBBBBBBBBBBBBBBBBBR│LBBBBR│ │LTTTTTTTR│LTTTTTTTTTTTTTTTTTTTTTTTR│LTTTTR│ │LDoubleRR│LLLLLLLdouble lineRRRRRRR│LL3RRR│ │LBBBBBBBR│LBBBBBBBBBBBBBBBBBBBBBBBR│LBBBBR│ └─────────┴─────────────────────────┴──────┘ ` visualCheck(t, "TableWithCustomPadding", buf.String(), expected) } func TestStreamBorders(t *testing.T) { data := [][]string{{"A", "B"}, {"C", "D"}} widths := map[int]int{0: 3, 1: 3} // Content (1) + padding (1+1) = 3 tests := []struct { name string borders tw.Border expected string }{ { name: "All Off", borders: tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off}, expected: ` A │ B C │ D `, }, { name: "No Left/Right", borders: tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.On, Bottom: tw.On}, expected: ` ───┬─── A │ B C │ D ───┴─── `, }, { name: "No Top/Bottom", borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.Off, Bottom: tw.Off}, expected: ` │ A │ B │ │ C │ D │ `, }, { name: "Only Left", borders: tw.Border{Left: tw.On, Right: tw.Off, Top: tw.Off, Bottom: tw.Off}, expected: ` │ A │ B │ C │ D `, }, { name: "Default", borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On}, expected: ` ┌───┬───┐ │ A │ B │ │ C │ D │ └───┴───┘ `, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var buf bytes.Buffer r := renderer.NewBlueprint( tw.Rendition{ Borders: tt.borders, }, ) st := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignLeft}, }, Widths: tw.CellWidth{PerColumn: widths}}), tablewriter.WithRenderer(r), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), ) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } st.Append(data[0]) st.Append(data[1]) err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } if !visualCheck(t, "StreamBorders_"+tt.name, buf.String(), tt.expected) { fmt.Printf("--- DEBUG LOG for %s ---\n", tt.name) fmt.Println(st.Debug().String()) t.Fail() } }) } } func TestAlignmentMigration(t *testing.T) { // Test new CellAlignment buf := &bytes.Buffer{} t.Run("NewCellAlignment", func(t *testing.T) { table := tablewriter.NewTable(buf) table.Configure(func(cfg *tablewriter.Config) { cfg.Header.Alignment.Global = tw.AlignCenter cfg.Row.Alignment.PerColumn = []tw.Align{tw.AlignLeft, tw.AlignRight} }) table.Header([]string{"Name", "Age Of User"}) table.Append([]string{"Alice Samsung", "30"}) table.Render() expected := ` ┌───────────────┬─────────────┐ │ NAME │ AGE OF USER │ ├───────────────┼─────────────┤ │ Alice Samsung │ 30 │ └───────────────┴─────────────┘ ` if !visualCheck(t, "New CellAlignment", buf.String(), expected) { t.Fatal("New CellAlignment rendering failed") } }) t.Run("DeprecatedAlignment", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(buf) table.Configure(func(cfg *tablewriter.Config) { cfg.Header.Formatting.Alignment = tw.AlignCenter cfg.Row.ColumnAligns = []tw.Align{tw.AlignLeft, tw.AlignRight} }) table.Header([]string{"Name", "Age Of User"}) table.Append([]string{"Alice Samsung", "30"}) table.Render() expected := ` ┌───────────────┬─────────────┐ │ NAME │ AGE OF USER │ ├───────────────┼─────────────┤ │ Alice Samsung │ 30 │ └───────────────┴─────────────┘ ` if !visualCheck(t, "Deprecated Alignment Fields", buf.String(), expected) { t.Fatal("Deprecated ColumnAligns and Formatting.Alignment rendering failed") } }) t.Run("TableConfigureBasic", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(buf) table.Configure(func(cfg *tablewriter.Config) { cfg.Header.Formatting.Alignment = tw.AlignLeft cfg.Row.Formatting.Alignment = tw.AlignRight }) table.Header([]string{"NAME", "DEPARTMENT", "SALARY"}) table.Append([]string{"Alice", "Engineering", "120000"}) table.Append([]string{"Bob", "Marketing", "85000"}) table.Render() expectedConfigure := ` ┌───────┬─────────────┬────────┐ │ NAME │ DEPARTMENT │ SALARY │ ├───────┼─────────────┼────────┤ │ Alice │ Engineering │ 120000 │ │ Bob │ Marketing │ 85000 │ └───────┴─────────────┴────────┘ ` if !visualCheck(t, "Table_Configure_Basic", buf.String(), expectedConfigure) { t.Fatal("Table_Configure_Basic rendering failed") } }) t.Run("HorizontalMergeEachLineCenter", func(t *testing.T) { // Test HorizontalMergeEachLineCenter scenario buf.Reset() table := tablewriter.NewTable(buf) table.Configure(func(cfg *tablewriter.Config) { cfg.Row.Formatting.Alignment = tw.AlignCenter cfg.Row.Formatting.MergeMode = tw.MergeHorizontal }) table.Header([]string{"DATE", "SECTION A", "SECTION B", "SECTION C", "SECTION D", "SECTION E"}) table.Append([]string{"1/1/2014", "apple", "boy", "cat", "dog", "elephant"}) table.Render() expectedMerge := ` ┌──────────┬───────────┬───────────┬───────────┬───────────┬───────────┐ │ DATE │ SECTION A │ SECTION B │ SECTION C │ SECTION D │ SECTION E │ ├──────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 1/1/2014 │ apple │ boy │ cat │ dog │ elephant │ └──────────┴───────────┴───────────┴───────────┴───────────┴───────────┘ ` if !visualCheck(t, "HorizontalMergeEachLineCenter", buf.String(), expectedMerge) { t.Fatal("HorizontalMergeEachLineCenter rendering failed") } }) t.Run("StreamBasic", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(buf) table.Configure(func(cfg *tablewriter.Config) { cfg.Footer.Alignment.Global = tw.AlignRight cfg.Stream.Enable = true }) table.Start() table.Header([]string{"NAME", "AGE", "CITY"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Footer([]string{"Total", "55", "*"}) table.Close() expectedStream := ` ┌────────┬────────┬────────┐ │ NAME │ AGE │ CITY │ ├────────┼────────┼────────┤ │ Alice │ 25 │ New │ │ │ │ York │ │ Bob │ 30 │ Boston │ ├────────┼────────┼────────┤ │ Total │ 55 │ * │ └────────┴────────┴────────┘ ` if !visualCheck(t, "StreamBasic", buf.String(), expectedStream) { t.Fatal("StreamBasic rendering failed") } }) } tablewriter-1.0.9/tests/blueprint_test.go000066400000000000000000000062501504170215200206010ustar00rootroot00000000000000package tests import ( "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "testing" ) func TestDefaultConfigMerging(t *testing.T) { tests := []struct { name string config tw.Rendition expected tw.Rendition }{ { name: "EmptyConfig", config: tw.Rendition{}, expected: tw.Rendition{ Borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On}, Settings: tw.Settings{ Separators: tw.Separators{ ShowHeader: tw.On, ShowFooter: tw.On, BetweenRows: tw.Off, BetweenColumns: tw.On, }, Lines: tw.Lines{ ShowTop: tw.On, ShowBottom: tw.On, ShowHeaderLine: tw.On, ShowFooterLine: tw.On, }, // TrimWhitespace: tw.On, CompactMode: tw.Off, }, Symbols: tw.NewSymbols(tw.StyleLight), }, }, { name: "PartialBorders", config: tw.Rendition{ Borders: tw.Border{Top: tw.Off}, }, expected: tw.Rendition{ Borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.Off, Bottom: tw.On}, Settings: tw.Settings{ Separators: tw.Separators{ ShowHeader: tw.On, ShowFooter: tw.On, BetweenRows: tw.Off, BetweenColumns: tw.On, }, Lines: tw.Lines{ ShowTop: tw.On, ShowBottom: tw.On, ShowHeaderLine: tw.On, ShowFooterLine: tw.On, }, // TrimWhitespace: tw.On, CompactMode: tw.Off, }, Symbols: tw.NewSymbols(tw.StyleLight), }, }, { name: "PartialSettingsLines", config: tw.Rendition{ Settings: tw.Settings{ Lines: tw.Lines{ShowFooterLine: tw.Off}, }, }, expected: tw.Rendition{ Borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On}, Settings: tw.Settings{ Separators: tw.Separators{ ShowHeader: tw.On, ShowFooter: tw.On, BetweenRows: tw.Off, BetweenColumns: tw.On, }, Lines: tw.Lines{ ShowTop: tw.On, ShowBottom: tw.On, ShowHeaderLine: tw.On, ShowFooterLine: tw.Off, }, // TrimWhitespace: tw.On, CompactMode: tw.Off, }, Symbols: tw.NewSymbols(tw.StyleLight), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := renderer.NewBlueprint(tt.config) got := r.Config() // Compare Borders if got.Borders != tt.expected.Borders { t.Errorf("%s: Borders mismatch - expected %+v, got %+v", tt.name, tt.expected.Borders, got.Borders) } // Compare Settings.Lines if got.Settings.Lines != tt.expected.Settings.Lines { t.Errorf("%s: Settings.Lines mismatch - expected %+v, got %+v", tt.name, tt.expected.Settings.Lines, got.Settings.Lines) } // Compare Settings.Separators if got.Settings.Separators != tt.expected.Settings.Separators { t.Errorf("%s: Settings.Separators mismatch - expected %+v, got %+v", tt.name, tt.expected.Settings.Separators, got.Settings.Separators) } // Check Symbols (basic presence check) if (tt.expected.Symbols == nil) != (got.Symbols == nil) { t.Errorf("%s: Symbols mismatch - expected nil: %v, got nil: %v", tt.name, tt.expected.Symbols == nil, got.Symbols == nil) } }) } } tablewriter-1.0.9/tests/bug_test.go000066400000000000000000000231711504170215200173530ustar00rootroot00000000000000package tests import ( "bytes" "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "testing" ) type Cleaner string // Note: Format() overrides String() if both exist. func (c Cleaner) Format() string { return clean(string(c)) } type Age int // Age int will be ignore and string will be used func (a Age) String() string { return fmt.Sprintf("%dyrs", a) } type Person struct { Name string Age int City string } type Profile struct { Name Cleaner Age Age City string } func TestBug252(t *testing.T) { var buf bytes.Buffer type Person struct { Name string Age int City string } header := []string{"Name", "Age", "City"} alice := Person{Name: "Alice", Age: 25, City: "New York"} bob := Profile{Name: Cleaner("Bo b"), Age: Age(30), City: "Boston"} t.Run("Normal", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithDebug(true)) table.Header(header) table.Append("Alice", "25", "New York") table.Append("Bob", "30", "Boston") table.Render() expected := ` ┌───────┬─────┬──────────┐ │ NAME │ AGE │ CITY │ ├───────┼─────┼──────────┤ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ └───────┴─────┴──────────┘ ` debug := visualCheck(t, "TestBug252-Normal", buf.String(), expected) if !debug { t.Error(table.Debug()) } }) t.Run("Mixed", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithDebug(true)) table.Header(header) table.Append(alice) table.Append("Bob", "30", "Boston") table.Render() expected := ` ┌───────┬─────┬──────────┐ │ NAME │ AGE │ CITY │ ├───────┼─────┼──────────┤ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ └───────┴─────┴──────────┘ ` debug := visualCheck(t, "TestBug252-Mixed", buf.String(), expected) if !debug { // t.Error(table.Debug()) } }) t.Run("Profile", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithDebug(true)) table.Header(header) table.Append(Cleaner("A lice"), Cleaner("2 5 yrs"), "New York") table.Append(bob) table.Render() expected := ` ┌───────┬───────┬──────────┐ │ NAME │ AGE │ CITY │ ├───────┼───────┼──────────┤ │ Alice │ 25yrs │ New York │ │ Bob │ 30yrs │ Boston │ └───────┴───────┴──────────┘ ` debug := visualCheck(t, "TestBasicTableDefault", buf.String(), expected) if !debug { // t.Error(table.Debug()) } }) type Override struct { Fish string `json:"name"` Name string `json:"-"` Age Age City string } t.Run("Override", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithDebug(true)) table.Header(header) table.Append(Cleaner("A lice"), Cleaner("2 5 yrs"), "New York") table.Append(Override{ Fish: "Bob", Name: "Skip", Age: Age(25), City: "Boston", }) table.Render() expected := ` ┌───────┬───────┬──────────┐ │ NAME │ AGE │ CITY │ ├───────┼───────┼──────────┤ │ Alice │ 25yrs │ New York │ │ Bob │ 25yrs │ Boston │ └───────┴───────┴──────────┘ ` debug := visualCheck(t, "TestBug252-Override", buf.String(), expected) if !debug { // t.Error(table.Debug()) } }) t.Run("Override-Streaming", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithDebug(true), tablewriter.WithStreaming(tw.StreamConfig{Enable: true})) err := table.Start() if err != nil { t.Error(err) } table.Header(header) table.Append(Cleaner("A lice"), Cleaner("2 5 yrs"), "New York") table.Append(Override{ Fish: "Bob", Name: "Skip", Age: Age(25), City: "Boston", }) expected := ` ┌────────┬────────┬────────┐ │ NAME │ AGE │ CITY │ ├────────┼────────┼────────┤ │ Alice │ 25yrs │ New │ │ │ │ York │ │ Bob │ 25yrs │ Boston │ └────────┴────────┴────────┘ ` err = table.Close() if err != nil { t.Error(err) } debug := visualCheck(t, "TestBug252-Override-Streaming", buf.String(), expected) if !debug { // t.Error(table.Debug()) } }) } func TestBug254(t *testing.T) { var buf bytes.Buffer data := [][]string{ {" LEFT", "RIGHT ", " BOTH "}, } t.Run("Normal", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithRowMaxWidth(20), tablewriter.WithTrimSpace(tw.On), tablewriter.WithAlignment(tw.Alignment{tw.AlignCenter, tw.AlignCenter, tw.AlignCenter}), ) table.Bulk(data) table.Render() expected := ` ┌──────┬───────┬──────┐ │ LEFT │ RIGHT │ BOTH │ └──────┴───────┴──────┘ ` debug := visualCheck(t, "TestBug252-Normal", buf.String(), expected) if !debug { t.Error(table.Debug()) } }) t.Run("Mixed", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithRowMaxWidth(20), tablewriter.WithTrimSpace(tw.Off), tablewriter.WithAlignment(tw.Alignment{tw.AlignCenter, tw.AlignCenter, tw.AlignCenter}), ) table.Bulk(data) table.Render() expected := ` ┌────────┬─────────┬──────────┐ │ LEFT │ RIGHT │ BOTH │ └────────┴─────────┴──────────┘ ` debug := visualCheck(t, "TestBug252-Mixed", buf.String(), expected) if !debug { t.Error(table.Debug()) } }) } func TestBug267(t *testing.T) { var buf bytes.Buffer data := [][]string{ {"a", "b", "c"}, {"aa", "bb", "cc"}, } t.Run("WithoutMaxWith", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithTrimSpace(tw.On), tablewriter.WithConfig(tablewriter.Config{Row: tw.CellConfig{Padding: tw.CellPadding{ Global: tw.PaddingNone, }}}), ) table.Bulk(data) table.Render() expected := ` ┌──┬──┬──┐ │a │b │c │ │aa│bb│cc│ └──┴──┴──┘ ` debug := visualCheck(t, "TestBug252-Normal", buf.String(), expected) if !debug { t.Error(table.Debug()) } }) t.Run("WithMaxWidth", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithRowMaxWidth(20), tablewriter.WithTrimSpace(tw.Off), tablewriter.WithAlignment(tw.Alignment{tw.AlignCenter, tw.AlignCenter, tw.AlignCenter}), tablewriter.WithDebug(false), tablewriter.WithConfig(tablewriter.Config{Row: tw.CellConfig{Padding: tw.CellPadding{ Global: tw.PaddingNone, }}}), ) table.Bulk(data) table.Render() expected := ` ┌──┬──┬──┐ │a │b │c │ │aa│bb│cc│ └──┴──┴──┘ ` debug := visualCheck(t, "TestBug252-Mixed", buf.String(), expected) if !debug { t.Error(table.Debug()) } }) } func TestBug271(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, }, Footer: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}}, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, })), ) table.Header([]string{"Info", "Info", "Info", "Info"}) table.Append([]string{"1/1/2014", "Domain name", "Successful", "Successful"}) table.Footer([]string{"", "", "TOTAL", "$145.93"}) // Fixed from Append table.Render() expected := ` ┌──────────────────────────────────────────────────┐ │ INFO │ ├──────────┬─────────────┬────────────┬────────────┤ │ 1/1/2014 │ Domain name │ Successful │ Successful │ ├──────────┴─────────────┴────────────┼────────────┤ │ TOTAL │ $145.93 │ └─────────────────────────────────────┴────────────┘ ` check := visualCheck(t, "HorizontalMergeAlignFooter", buf.String(), expected) if !check { t.Error(table.Debug()) } } tablewriter-1.0.9/tests/caption_test.go000066400000000000000000000200531504170215200202270ustar00rootroot00000000000000// File: tests/caption_test.go package tests import ( "bytes" "github.com/olekukonko/tablewriter/renderer" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" // Assuming your tw types (CaptionPosition, Align) are here ) // TestTableCaptions comprehensive tests for caption functionality func TestTableCaptions(t *testing.T) { data := [][]string{ {"Alice", "30", "New York"}, {"Bob", "24", "San Francisco"}, {"Charlie", "35", "London"}, } headers := []string{"Name", "Age", "City"} shortCaption := "User Data" longCaption := "This is a detailed caption for the user data table, intended to demonstrate text wrapping and alignment features." baseTableSetup := func(buf *bytes.Buffer) *tablewriter.Table { table := tablewriter.NewTable(buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: tw.NewSymbols(tw.StyleASCII)})), tablewriter.WithDebug(true), ) table.Header(headers) for _, v := range data { table.Append(v) } return table } t.Run("NoCaption", func(t *testing.T) { var buf bytes.Buffer table := baseTableSetup(&buf) table.Render() expected := ` +---------+-----+---------------+ | NAME | AGE | CITY | +---------+-----+---------------+ | Alice | 30 | New York | | Bob | 24 | San Francisco | | Charlie | 35 | London | +---------+-----+---------------+ ` if !visualCheckCaption(t, t.Name(), buf.String(), expected) { t.Log(table.Debug().String()) } }) t.Run("LegacySetCaption_BottomCenter", func(t *testing.T) { var buf bytes.Buffer table := baseTableSetup(&buf) table.Caption(tw.Caption{Text: shortCaption}) // Legacy, defaults to BottomCenter, auto width table.Render() // Width of table: 7+3+15 + 4 borders/separators = 29. "User Data" is 9. // (29-9)/2 = 10 padding left. expected := ` +---------+-----+---------------+ | NAME | AGE | CITY | +---------+-----+---------------+ | Alice | 30 | New York | | Bob | 24 | San Francisco | | Charlie | 35 | London | +---------+-----+---------------+ User Data ` if !visualCheckCaption(t, t.Name(), buf.String(), expected) { t.Log(table.Debug().String()) } }) t.Run("CaptionBottomCenter_AutoWidthBottom", func(t *testing.T) { var buf bytes.Buffer table := baseTableSetup(&buf) table.Caption(tw.Caption{Text: shortCaption, Spot: tw.SpotBottomCenter, Align: tw.AlignCenter}) table.Render() expected := ` +---------+-----+---------------+ | NAME | AGE | CITY | +---------+-----+---------------+ | Alice | 30 | New York | | Bob | 24 | San Francisco | | Charlie | 35 | London | +---------+-----+---------------+ User Data ` if !visualCheckCaption(t, t.Name(), buf.String(), expected) { t.Log(table.Debug().String()) } }) t.Run("CaptionTopCenter_AutoWidthTop", func(t *testing.T) { var buf bytes.Buffer table := baseTableSetup(&buf) table.Caption(tw.Caption{Text: shortCaption, Spot: tw.SpotTopCenter, Align: tw.AlignCenter}) table.Render() expected := ` User Data +---------+-----+---------------+ | NAME | AGE | CITY | +---------+-----+---------------+ | Alice | 30 | New York | | Bob | 24 | San Francisco | | Charlie | 35 | London | +---------+-----+---------------+ ` if !visualCheckCaption(t, t.Name(), buf.String(), expected) { t.Log(table.Debug().String()) } }) t.Run("CaptionBottomLeft_AutoWidth", func(t *testing.T) { var buf bytes.Buffer table := baseTableSetup(&buf) table.Caption(tw.Caption{Text: shortCaption, Spot: tw.SpotBottomLeft, Align: tw.AlignLeft}) table.Render() expected := ` +---------+-----+---------------+ | NAME | AGE | CITY | +---------+-----+---------------+ | Alice | 30 | New York | | Bob | 24 | San Francisco | | Charlie | 35 | London | +---------+-----+---------------+ User Data ` if !visualCheckCaption(t, t.Name(), buf.String(), expected) { t.Log(table.Debug().String()) } }) t.Run("CaptionTopRight_AutoWidth", func(t *testing.T) { var buf bytes.Buffer table := baseTableSetup(&buf) table.Caption(tw.Caption{Text: shortCaption, Spot: tw.SpotTopRight, Align: tw.AlignRight}) table.Render() expected := ` User Data +---------+-----+---------------+ | NAME | AGE | CITY | +---------+-----+---------------+ | Alice | 30 | New York | | Bob | 24 | San Francisco | | Charlie | 35 | London | +---------+-----+---------------+ ` if !visualCheckCaption(t, t.Name(), buf.String(), expected) { t.Log(table.Debug().String()) } }) t.Run("CaptionBottomCenter_LongCaption_AutoWidth", func(t *testing.T) { var buf bytes.Buffer table := baseTableSetup(&buf) table.Caption(tw.Caption{Text: longCaption, Spot: tw.SpotBottomCenter, Align: tw.AlignCenter}) table.Render() // Table width is 29. Long caption will wrap to this. // "This is a detailed caption for" (29) // "the user data table, intended" (29) // "to demonstrate text wrapping" (28) // "and alignment features." (25) expected := ` +---------+-----+---------------+ | NAME | AGE | CITY | +---------+-----+---------------+ | Alice | 30 | New York | | Bob | 24 | San Francisco | | Charlie | 35 | London | +---------+-----+---------------+ This is a detailed caption for the user data table, intended to demonstrate text wrapping and alignment features. ` if !visualCheckCaption(t, t.Name(), buf.String(), expected) { t.Log(table.Debug().String()) } }) t.Run("CaptionTopLeft_LongCaption_MaxWidth20", func(t *testing.T) { var buf bytes.Buffer table := baseTableSetup(&buf) // captionMaxWidth 20, table width is 29. Caption wraps to 20. Padded to 29 (table width) for alignment. table.Caption(tw.Caption{Text: longCaption, Spot: tw.SpotTopLeft, Align: tw.AlignLeft, Width: 20}) table.Render() // The visual check normalizes spaces, so the alignment padding to table width is tricky to test visually for left/right aligned captions. // It's more about the wrapping width for the caption text itself. // The printTopBottomCaption will align the *block* of wrapped text. // Let's adjust the expected output: the lines are padded to actualTableWidth. // The caption lines themselves are max 20 wide. expectedAdjusted := ` This is a detailed caption for the user data table, intended to demonstrate text wrapping and alignment features. +---------+-----+---------------+ | NAME | AGE | CITY | +---------+-----+---------------+ | Alice | 30 | New York | | Bob | 24 | San Francisco | | Charlie | 35 | London | +---------+-----+---------------+ ` if !visualCheckCaption(t, t.Name(), buf.String(), expectedAdjusted) { t.Log(table.Debug().String()) } }) t.Run("CaptionBottomCenter_EmptyTable", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewWriter(&buf) // No header, no data table.Caption(tw.Caption{Text: shortCaption, Spot: tw.SpotBottomCenter, Align: tw.AlignCenter}) table.Render() // Expected: table is empty box, caption centered to its own width or a default. // Empty table with default borders prints: // +--+ // +--+ // If actualTableWidth is 0, captionWrapWidth becomes natural width of caption (9 for "User Data") // Then paddingTargetWidth also becomes 9. expected := ` +--+ +--+ User Data ` if !visualCheckCaption(t, t.Name(), buf.String(), expected) { t.Log(table.Debug().String()) } }) t.Run("CaptionTopLeft_EmptyTable_MaxWidth10", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewWriter(&buf) table.Caption(tw.Caption{Text: "A very long caption text.", Spot: tw.SpotTopLeft, Align: tw.AlignLeft, Width: 10}) table.Render() // Table is empty, captionMaxWidth is 10. // "A very" // "long" // "caption" // "text." // Each line left-aligned within width 10. expected := ` A very long caption text. +--+ +--+ ` if !visualCheckCaption(t, t.Name(), buf.String(), expected) { t.Log(table.Debug().String()) } }) } tablewriter-1.0.9/tests/colorized_test.go000066400000000000000000000142731504170215200205730ustar00rootroot00000000000000package tests import ( "bytes" "testing" "github.com/fatih/color" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) func TestColorizedBasicTable(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewColorized()), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() // Expected colors: Headers (white/bold on black), Rows (cyan on black), Borders/Separators (white on black) expected := ` ┌───────┬─────┬──────────┐ │ NAME │ AGE │ CITY │ ├───────┼─────┼──────────┤ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ └───────┴─────┴──────────┘ ` visualCheck(t, "ColorizedBasicTable", buf.String(), expected) } func TestColorizedNoBorders(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewColorized(renderer.ColorizedConfig{ Borders: tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off}, })), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() // Expected colors: Headers (white/bold on black), Rows (cyan on black), Separators (white on black) expected := ` NAME │ AGE │ CITY ───────┼─────┼────────── Alice │ 25 │ New York Bob │ 30 │ Boston ` visualCheck(t, "ColorizedNoBorders", buf.String(), expected) } func TestColorizedCustomColors(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewColorized(renderer.ColorizedConfig{ Header: renderer.Tint{ FG: renderer.Colors{color.FgGreen, color.Bold}, BG: renderer.Colors{color.BgBlue}, Columns: []renderer.Tint{ {FG: renderer.Colors{color.FgRed}, BG: renderer.Colors{color.BgBlue}}, {FG: renderer.Colors{color.FgYellow}, BG: renderer.Colors{color.BgBlue}}, }, }, Column: renderer.Tint{ FG: renderer.Colors{color.FgBlue}, BG: renderer.Colors{color.BgBlack}, Columns: []renderer.Tint{ {FG: renderer.Colors{color.FgMagenta}, BG: renderer.Colors{color.BgBlack}}, }, }, Footer: renderer.Tint{ FG: renderer.Colors{color.FgYellow}, BG: renderer.Colors{color.BgBlue}, }, Border: renderer.Tint{ FG: renderer.Colors{color.FgWhite}, BG: renderer.Colors{color.BgBlue}, }, Separator: renderer.Tint{ FG: renderer.Colors{color.FgWhite}, BG: renderer.Colors{color.BgBlue}, }, })), tablewriter.WithFooterConfig(tw.CellConfig{ Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignRight, tw.AlignCenter}}, }), ) table.Header([]string{"Name", "Age"}) table.Append([]string{"Alice", "25"}) table.Footer([]string{"Total", "1"}) table.Render() // Expected colors: Headers (red, yellow on blue), Rows (magenta, blue on black), Footers (yellow on blue), Borders/Separators (white on blue) expected := ` ┌───────┬─────┐ │ NAME │ AGE │ ├───────┼─────┤ │ Alice │ 25 │ ├───────┼─────┤ │ Total │ 1 │ └───────┴─────┘ ` if !visualCheck(t, "ColorizedCustomColors", buf.String(), expected) { t.Error(table.Debug()) } } func TestColorizedLongValues(t *testing.T) { var buf bytes.Buffer c := tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapNormal, }, Alignment: tw.CellAlignment{Global: tw.AlignLeft}, ColMaxWidths: tw.CellWidth{Global: 20}, }, } table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c), tablewriter.WithRenderer(renderer.NewColorized()), ) table.Header([]string{"No", "Description", "Note"}) table.Append([]string{"1", "This is a very long description that should wrap", "Short"}) table.Append([]string{"2", "Short desc", "Another note"}) table.Render() // Expected colors: Headers (white/bold on black), Rows (cyan on black), Borders/Separators (white on black) expected := ` ┌────┬──────────────────┬──────────────┐ │ NO │ DESCRIPTION │ NOTE │ ├────┼──────────────────┼──────────────┤ │ 1 │ This is a very │ Short │ │ │ long description │ │ │ │ that should wrap │ │ │ 2 │ Short desc │ Another note │ └────┴──────────────────┴──────────────┘ ` visualCheck(t, "ColorizedLongValues", buf.String(), expected) } func TestColorizedHorizontalMerge(t *testing.T) { var buf bytes.Buffer c := tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, }, } table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c), tablewriter.WithRenderer(renderer.NewColorized()), ) table.Header([]string{"Merged", "Merged", "Normal"}) table.Append([]string{"Same", "Same", "Unique"}) table.Render() // Expected colors: Headers (white/bold on black), Rows (cyan on black), Borders/Separators (white on black) expected := ` ┌─────────────────┬────────┐ │ MERGED │ NORMAL │ ├─────────────────┼────────┤ │ Same │ Unique │ └─────────────────┴────────┘ ` if !visualCheck(t, "ColorizedHorizontalMerge", buf.String(), expected) { t.Error(table.Debug()) } } tablewriter-1.0.9/tests/csv_test.go000066400000000000000000000305721504170215200173740ustar00rootroot00000000000000package tests // Use _test package to test as a user import ( "bytes" "encoding/csv" "strings" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" // For direct renderer use if needed "github.com/olekukonko/tablewriter/tw" ) const csvTestData = `Name,Department,Salary Alice,Engineering,120000 Bob,Marketing,85000 Charlie,Engineering,135000 Diana,HR,70000 ` func getCSVReaderFromString(data string) *csv.Reader { stringReader := strings.NewReader(data) return csv.NewReader(stringReader) } func TestTable_Configure_Basic(t *testing.T) { var buf bytes.Buffer csvReader := getCSVReaderFromString(csvTestData) table, err := tablewriter.NewCSVReader(&buf, csvReader, true) if err != nil { t.Fatalf("NewCSVReader failed: %v", err) } // Check initial default config values (examples) if table.Config().Header.Alignment.Global != tw.AlignCenter { t.Errorf("Expected initial header alignment to be Center, got %s", table.Config().Header.Alignment.Global) } if table.Config().Behavior.TrimSpace != tw.On { // Default from defaultConfig() t.Errorf("Expected initial TrimSpace to be On, got %s", table.Config().Behavior.TrimSpace) } table.Configure(func(cfg *tablewriter.Config) { cfg.Header.Alignment.Global = tw.AlignLeft cfg.Row.Alignment.Global = tw.AlignRight cfg.Behavior.TrimSpace = tw.Off cfg.Debug = true // This should enable the logger }) // Check that Table.config was updated if table.Config().Header.Alignment.Global != tw.AlignLeft { t.Errorf("Expected configured header alignment to be Left, got %s", table.Config().Header.Alignment.Global) } if table.Config().Row.Alignment.Global != tw.AlignRight { t.Errorf("Expected configured row alignment to be Right, got %s", table.Config().Row.Alignment.Global) } if table.Config().Behavior.TrimSpace != tw.Off { t.Errorf("Expected configured TrimSpace to be Off, got %s", table.Config().Behavior.TrimSpace) } if !table.Config().Debug { t.Errorf("Expected configured Debug to be true") } // Render and check output (visual check will confirm alignment and trimming) err = table.Render() if err != nil { t.Fatalf("Render failed: %v", err) } // What visualCheck will see (assuming Blueprint respects CellContext.Align passed from table.config): expectedAfterConfigure := ` ┌─────────┬─────────────┬────────┐ │ NAME │ DEPARTMENT │ SALARY │ ├─────────┼─────────────┼────────┤ │ Alice │ Engineering │ 120000 │ │ Bob │ Marketing │ 85000 │ │ Charlie │ Engineering │ 135000 │ │ Diana │ HR │ 70000 │ └─────────┴─────────────┴────────┘ ` if !visualCheck(t, "TestTable_Configure_Basic", buf.String(), expectedAfterConfigure) { t.Logf("Debug trace from table:\n%s", table.Debug().String()) } } func TestTable_Options_WithRendition_Borderless(t *testing.T) { var buf bytes.Buffer csvReader := getCSVReaderFromString(csvTestData) // Ensure csvTestData is defined table, err := tablewriter.NewCSVReader(&buf, csvReader, true, tablewriter.WithDebug(true)) if err != nil { t.Fatalf("NewCSVReader failed: %v", err) } // Initially, it should have default borders table.Render() initialOutputWithBorders := buf.String() buf.Reset() // Clear buffer for next render if !strings.Contains(initialOutputWithBorders, "┌───") { // Basic check for default border t.Errorf("Expected initial render to have borders, but got:\n%s", initialOutputWithBorders) } // Define a TRULY borderless and line-less rendition borderlessRendition := tw.Rendition{ Borders: tw.Border{ // Explicitly set all borders to Off Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off, }, // Using StyleNone for symbols means no visible characters for borders/lines if they were on. // For a "markdown-like but no lines" look, you might use StyleMarkdown and then turn off lines/separators. // For true "no visual structure", StyleNone is good. Symbols: tw.NewSymbols(tw.StyleNone), Settings: tw.Settings{ Lines: tw.Lines{ // Explicitly set all line drawing to Off ShowTop: tw.Off, ShowBottom: tw.Off, ShowHeaderLine: tw.Off, ShowFooterLine: tw.Off, }, Separators: tw.Separators{ // Explicitly set all separators to Off ShowHeader: tw.Off, ShowFooter: tw.Off, BetweenRows: tw.Off, BetweenColumns: tw.Off, }, }, } table.Options( tablewriter.WithRendition(borderlessRendition), ) // Render again err = table.Render() if err != nil { t.Fatalf("Render after WithRendition failed: %v", err) } // Expected output: Plain text, no borders, no lines, no separators. // Content alignment will be default (Header:Center, Row:Left) because // Table.config was not modified for alignments in this test. expectedOutputBorderless := ` NAME DEPARTMENT SALARY Alice Engineering 120000 Bob Marketing 85000 Charlie Engineering 135000 Diana HR 70000 ` if !visualCheck(t, "TestTable_Options_WithRendition_Borderless", buf.String(), expectedOutputBorderless) { t.Logf("Initial output with borders was:\n%s", initialOutputWithBorders) // For context t.Logf("Debug trace from table after borderless rendition:\n%s", table.Debug().String()) } // Verify renderer's internal config was changed if bp, ok := table.Renderer().(*renderer.Blueprint); ok { currentRendererCfg := bp.Config() if currentRendererCfg.Borders.Left.Enabled() { t.Errorf("Blueprint Borders.Left should be OFF, but is ON") } if currentRendererCfg.Borders.Right.Enabled() { t.Errorf("Blueprint Borders.Right should be OFF, but is ON") } if currentRendererCfg.Borders.Top.Enabled() { t.Errorf("Blueprint Borders.Top should be OFF, but is ON") } if currentRendererCfg.Borders.Bottom.Enabled() { t.Errorf("Blueprint Borders.Bottom should be OFF, but is ON") } if currentRendererCfg.Settings.Lines.ShowHeaderLine.Enabled() { t.Errorf("Blueprint Settings.Lines.ShowHeaderLine should be OFF, but is ON") } if currentRendererCfg.Settings.Lines.ShowTop.Enabled() { t.Errorf("Blueprint Settings.Lines.ShowTop should be OFF, but is ON") } if currentRendererCfg.Settings.Separators.BetweenColumns.Enabled() { t.Errorf("Blueprint Settings.Separators.BetweenColumns should be OFF, but is ON") } // Check symbols if relevant (StyleNone should have empty symbols) if currentRendererCfg.Symbols.Column() != "" { t.Errorf("Blueprint Symbols.Column should be empty for StyleNone, got '%s'", currentRendererCfg.Symbols.Column()) } } else { t.Logf("Renderer is not *renderer.Blueprint, skipping detailed internal config check. Type is %T", table.Renderer()) } } // Assume csvTestData and getCSVReaderFromString are defined as in previous examples: const csvTestDataForPartial = `Name,Department,Salary Alice,Engineering,120000 Bob,Marketing,85000 ` func getCSVReaderFromStringForPartial(data string) *csv.Reader { stringReader := strings.NewReader(data) return csv.NewReader(stringReader) } // Assume csvTestDataForPartial and getCSVReaderFromStringForPartial are defined const csvTestDataForPartialUpdate = `Name,Department,Salary Alice,Engineering,120000 Bob,Marketing,85000 ` func getCSVReaderFromStringForPartialUpdate(data string) *csv.Reader { stringReader := strings.NewReader(data) return csv.NewReader(stringReader) } func TestTable_Options_WithRendition_PartialUpdate(t *testing.T) { var buf bytes.Buffer csvReader := getCSVReaderFromStringForPartialUpdate(csvTestDataForPartialUpdate) // 1. Define an explicitly borderless and line-less initial rendition initiallyAllOffRendition := tw.Rendition{ Borders: tw.Border{ Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off, }, Symbols: tw.NewSymbols(tw.StyleNone), // StyleNone should render no visible symbols Settings: tw.Settings{ Lines: tw.Lines{ ShowTop: tw.Off, ShowBottom: tw.Off, ShowHeaderLine: tw.Off, ShowFooterLine: tw.Off, }, Separators: tw.Separators{ ShowHeader: tw.Off, ShowFooter: tw.Off, BetweenRows: tw.Off, BetweenColumns: tw.Off, }, }, } // Create table with this explicitly "all off" rendition table, err := tablewriter.NewCSVReader(&buf, csvReader, true, tablewriter.WithDebug(true), tablewriter.WithRenderer(renderer.NewBlueprint(initiallyAllOffRendition)), ) if err != nil { t.Fatalf("NewCSVReader with initial 'all off' rendition failed: %v", err) } // Render to confirm initial state (should be very plain) table.Render() outputAfterInitialAllOff := buf.String() buf.Reset() // Clear buffer for the next render // Check the initial plain output (content only, no borders/lines) expectedInitialPlainOutput := ` NAME DEPARTMENT SALARY Alice Engineering 120000 Bob Marketing 85000 ` if !visualCheck(t, "TestTable_Options_WithRendition_PartialUpdate_InitialState", outputAfterInitialAllOff, expectedInitialPlainOutput) { t.Errorf("Initial render was not plain as expected.") t.Logf("Initial 'all off' output was:\n%s", outputAfterInitialAllOff) } partialRenditionUpdate := tw.Rendition{ Borders: tw.Border{Top: tw.On, Bottom: tw.On}, // Left/Right are 0 (unspecified in this struct literal) Symbols: tw.NewSymbols(tw.StyleHeavy), Settings: tw.Settings{ Lines: tw.Lines{ShowTop: tw.On, ShowBottom: tw.On}, // Enable drawing of these lines // Separators are zero-value, so they will remain Off from 'initiallyAllOffRendition' }, } // Apply the partial update using Options table.Options( tablewriter.WithRendition(partialRenditionUpdate), ) // Render again err = table.Render() if err != nil { t.Fatalf("Render after partial WithRendition failed: %v", err) } outputAfterPartialUpdate := buf.String() expectedOutputPartialBorders := ` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ NAME DEPARTMENT SALARY Alice Engineering 120000 Bob Marketing 85000 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ` if !visualCheck(t, "TestTable_Options_WithRendition_PartialUpdate_FinalState", outputAfterPartialUpdate, expectedOutputPartialBorders) { t.Logf("Initial 'all off' output was:\n%s", outputAfterInitialAllOff) // For context t.Logf("Debug trace from table after partial update:\n%s", table.Debug().String()) } // 3. Verify the renderer's internal configuration reflects the partial update correctly. if bp, ok := table.Renderer().(*renderer.Blueprint); ok { currentRendererCfg := bp.Config() if !currentRendererCfg.Borders.Top.Enabled() { t.Errorf("Blueprint Borders.Top should be ON, but is OFF") } if !currentRendererCfg.Borders.Bottom.Enabled() { t.Errorf("Blueprint Borders.Bottom should be ON, but is OFF") } if currentRendererCfg.Borders.Left.Enabled() { t.Errorf("Blueprint Borders.Left should remain OFF, but is ON") } if currentRendererCfg.Borders.Right.Enabled() { t.Errorf("Blueprint Borders.Right should remain OFF, but is ON") } if currentRendererCfg.Symbols.Row() != "━" { // From StyleHeavy t.Errorf("Blueprint Symbols.Row is not '━' (Heavy), got '%s'", currentRendererCfg.Symbols.Row()) } // Column symbol check might be less relevant if BetweenColumns is Off, but good for completeness. if currentRendererCfg.Symbols.Column() != "┃" { // From StyleHeavy t.Errorf("Blueprint Symbols.Column is not '┃' (Heavy), got '%s'", currentRendererCfg.Symbols.Column()) } // Check Settings.Lines if !currentRendererCfg.Settings.Lines.ShowTop.Enabled() { t.Errorf("Blueprint Settings.Lines.ShowTop should be ON, but is OFF") } if !currentRendererCfg.Settings.Lines.ShowBottom.Enabled() { t.Errorf("Blueprint Settings.Lines.ShowBottom should be ON, but is OFF") } if currentRendererCfg.Settings.Lines.ShowHeaderLine.Enabled() { t.Errorf("Blueprint Settings.Lines.ShowHeaderLine should remain OFF, but is ON") } // Check Settings.Separators if currentRendererCfg.Settings.Separators.BetweenColumns.Enabled() { t.Errorf("Blueprint Settings.Separators.BetweenColumns should remain OFF, but is ON") } } else { t.Logf("Renderer is not *renderer.Blueprint, skipping detailed internal config check. Type is %T", table.Renderer()) } } tablewriter-1.0.9/tests/extra_test.go000066400000000000000000000403261504170215200177220ustar00rootroot00000000000000package tests import ( "bytes" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "testing" ) func TestFilterMasking(t *testing.T) { tests := []struct { name string filter tw.Filter data [][]string expected string }{ { name: "MaskEmail", filter: MaskEmail, data: [][]string{ {"Alice", "alice@example.com", "25"}, {"Bob", "bob.test@domain.org", "30"}, }, expected: ` ┌───────┬─────────────────────┬─────┐ │ NAME │ EMAIL │ AGE │ ├───────┼─────────────────────┼─────┤ │ Alice │ a****@example.com │ 25 │ │ Bob │ b*******@domain.org │ 30 │ └───────┴─────────────────────┴─────┘ `, }, { name: "MaskPassword", filter: MaskPassword, data: [][]string{ {"Alice", "secretpassword", "25"}, {"Bob", "pass1234", "30"}, }, expected: ` ┌───────┬────────────────┬─────┐ │ NAME │ PASSWORD │ AGE │ ├───────┼────────────────┼─────┤ │ Alice │ ************** │ 25 │ │ Bob │ ******** │ 30 │ └───────┴────────────────┴─────┘ `, }, { name: "MaskCard", filter: MaskCard, data: [][]string{ {"Alice", "4111-1111-1111-1111", "25"}, {"Bob", "5105105105105100", "30"}, }, expected: ` ┌───────┬─────────────────────┬─────┐ │ NAME │ CREDIT CARD │ AGE │ ├───────┼─────────────────────┼─────┤ │ Alice │ ****-****-****-1111 │ 25 │ │ Bob │ 5105105105105100 │ 30 │ └───────┴─────────────────────┴─────┘ `, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{AutoFormat: tw.On}, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, Padding: tw.CellPadding{Global: tw.Padding{Left: " ", Right: " "}}, }, Row: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignLeft}, Padding: tw.CellPadding{Global: tw.Padding{Left: " ", Right: " "}}, Filter: tw.CellFilter{ Global: tt.filter, }, }, })) header := []string{"Name", tt.name, "Age"} if tt.name == "MaskEmail" { header[1] = "Email" } else if tt.name == "MaskPassword" { header[1] = "Password" } else if tt.name == "MaskCard" { header[1] = "Credit Card" } table.Header(header) table.Bulk(tt.data) table.Render() visualCheck(t, tt.name, buf.String(), tt.expected) }) } } func TestMasterClass(t *testing.T) { var buf bytes.Buffer littleConfig := tablewriter.Config{ MaxWidth: 30, Row: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignCenter}, Padding: tw.CellPadding{ Global: tw.Padding{Left: tw.Skip, Right: tw.Skip, Top: tw.Skip, Bottom: tw.Skip}, }, }, } bigConfig := tablewriter.Config{ MaxWidth: 50, Header: tw.CellConfig{Formatting: tw.CellFormatting{ AutoWrap: tw.WrapTruncate, }}, Row: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignCenter}, Padding: tw.CellPadding{ Global: tw.Padding{Left: tw.Skip, Right: tw.Skip, Top: tw.Skip, Bottom: tw.Skip}, }, }, } little := func(s string) string { var b bytes.Buffer table := tablewriter.NewTable(&b, tablewriter.WithConfig(littleConfig), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Borders: tw.BorderNone, Settings: tw.Settings{ Separators: tw.Separators{ ShowHeader: tw.Off, ShowFooter: tw.Off, BetweenRows: tw.On, BetweenColumns: tw.Off, }, Lines: tw.Lines{ ShowTop: tw.Off, ShowBottom: tw.Off, ShowHeaderLine: tw.Off, ShowFooterLine: tw.On, }, }, })), ) table.Append([]string{s, s}) table.Append([]string{s, s}) table.Render() return b.String() } table := tablewriter.NewTable(&buf, tablewriter.WithConfig(bigConfig), tablewriter.WithDebug(true), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Borders: tw.BorderNone, Settings: tw.Settings{ Separators: tw.Separators{ ShowHeader: tw.Off, ShowFooter: tw.Off, BetweenRows: tw.Off, BetweenColumns: tw.On, }, Lines: tw.Lines{ ShowTop: tw.Off, ShowBottom: tw.Off, ShowHeaderLine: tw.Off, ShowFooterLine: tw.Off, }, }, })), ) table.Append([]string{little("A"), little("B")}) table.Append([]string{little("C"), little("D")}) table.Render() expected := ` A A │ B B ────── │ ────── A A │ B B C C │ D D ────── │ ────── C C │ D D ` visualCheck(t, "Master Class", buf.String(), expected) } func TestConfigAutoHideDefault(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf) // Use the new exported Config() method cfg := table.Config() if cfg.Behavior.AutoHide.Enabled() { t.Errorf("Expected AutoHide default to be false, got true") } } func TestAutoHideFeature(t *testing.T) { data := [][]string{ {"A", "The Good", ""}, // Rating is empty {"B", "The Bad", " "}, // Rating is whitespace {"C", "The Ugly", " "}, // Rating is whitespace {"D", "The Gopher", ""}, // Rating is empty // Add a row where Rating is NOT empty to test the opposite case {"E", "The Tester", "999"}, } // Test Case 1: Hide Empty Column t.Run("HideWhenEmpty", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithAutoHide(tw.On), // Enable the feature tablewriter.WithDebug(false), ) table.Header([]string{"Name", "Sign", "Rating"}) // Header IS included // Use only data where the last column IS empty emptyData := [][]string{ {"A", "The Good", ""}, {"B", "The Bad", " "}, {"C", "The Ugly", " "}, {"D", "The Gopher", ""}, } for _, v := range emptyData { table.Append(v) } table.Render() // Expected output: Rating column should be completely gone expected := ` ┌──────┬────────────┐ │ NAME │ SIGN │ ├──────┼────────────┤ │ A │ The Good │ │ B │ The Bad │ │ C │ The Ugly │ │ D │ The Gopher │ └──────┴────────────┘ ` // Use visualCheck, expect it might fail initially if Blueprint isn't perfect yet if !visualCheck(t, "AutoHide_HideWhenEmpty", buf.String(), expected) { t.Log("Output for HideWhenEmpty was not as expected (might be OK if Blueprint needs more fixes):") t.Error(buf.String()) // Log debug info if helpful // for _, v := range table.Debug() { // t.Log(v) // } } }) // Test Case 2: Show Column When Not Empty t.Run("ShowWhenNotEmpty", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithAutoHide(tw.On), // Feature enabled // tablewriter.WithRenderer(renderer.NewBlueprint()), ) table.Header([]string{"Name", "Sign", "Rating"}) // Use data where at least one row has content in the last column for _, v := range data { // Use the original data mix table.Append(v) } table.Render() // Expected output: Rating column should be present because row "E" has content expected := ` ┌──────┬────────────┬────────┐ │ NAME │ SIGN │ RATING │ ├──────┼────────────┼────────┤ │ A │ The Good │ │ │ B │ The Bad │ │ │ C │ The Ugly │ │ │ D │ The Gopher │ │ │ E │ The Tester │ 999 │ └──────┴────────────┴────────┘ ` if !visualCheck(t, "AutoHide_ShowWhenNotEmpty", buf.String(), expected) { t.Log("Output for ShowWhenNotEmpty was not as expected (might be OK if Blueprint needs more fixes):") t.Log(buf.String()) // Log debug info if helpful // for _, v := range table.Debug() { // t.Log(v) // } } }) // Test Case 3: Feature Disabled t.Run("DisabledShowsEmpty", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithAutoHide(tw.Off), // Feature explicitly disabled // tablewriter.WithRenderer(renderer.NewBlueprint()), ) table.Header([]string{"Name", "Sign", "Rating"}) // Use only data where the last column IS empty emptyData := [][]string{ {"A", "The Good", ""}, {"B", "The Bad", " "}, {"C", "The Ugly", " "}, {"D", "The Gopher", ""}, } for _, v := range emptyData { table.Append(v) } table.Render() // Expected output: Rating column should be present but empty expected := ` ┌──────┬────────────┬────────┐ │ NAME │ SIGN │ RATING │ ├──────┼────────────┼────────┤ │ A │ The Good │ │ │ B │ The Bad │ │ │ C │ The Ugly │ │ │ D │ The Gopher │ │ └──────┴────────────┴────────┘ ` // This one should ideally PASS if the default behavior is preserved if !visualCheck(t, "AutoHide_DisabledShowsEmpty", buf.String(), expected) { t.Errorf("AutoHide disabled test failed!") t.Log(buf.String()) // Log debug info if helpful // for _, v := range table.Debug() { // t.Log(v) // } } }) } func TestEmojiTable(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf) table.Header([]string{"Name 😺", "Age 🎂", "City 🌍"}) data := [][]string{ {"Alice 😊", "25", "New York 🌆"}, {"Bob 😎", "30", "Boston 🏙️"}, {"Charlie 🤓", "28", "Tokyo 🗼"}, } table.Bulk(data) table.Footer([]string{"", "Total 👥", "3"}) table.Configure(func(config *tablewriter.Config) { config.Row.Alignment.Global = tw.AlignLeft config.Footer.Alignment.Global = tw.AlignRight }) table.Render() expected := ` ┌────────────┬──────────┬─────────────┐ │ NAME 😺 │ AGE 🎂 │ CITY 🌍 │ ├────────────┼──────────┼─────────────┤ │ Alice 😊 │ 25 │ New York 🌆 │ │ Bob 😎 │ 30 │ Boston 🏙️ │ │ Charlie 🤓 │ 28 │ Tokyo 🗼 │ ├────────────┼──────────┼─────────────┤ │ │ Total 👥 │ 3 │ └────────────┴──────────┴─────────────┘ ` if !visualCheck(t, "EmojiTable", buf.String(), expected) { t.Error(table.Debug()) } } func TestUnicodeTableDefault(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bøb", "30", "Tōkyō"}) // Contains ø and ō table.Append([]string{"José", "28", "México"}) // Contains é and accented e (e + combining acute) table.Append([]string{"张三", "35", "北京"}) // Chinese characters table.Append([]string{"अनु", "40", "मुंबई"}) // Devanagari script table.Render() expected := ` ┌───────┬─────┬──────────┐ │ NAME │ AGE │ CITY │ ├───────┼─────┼──────────┤ │ Alice │ 25 │ New York │ │ Bøb │ 30 │ Tōkyō │ │ José │ 28 │ México │ │ 张三 │ 35 │ 北京 │ │ अनु │ 40 │ मुंबई │ └───────┴─────┴──────────┘ ` visualCheck(t, "UnicodeTableRendering", buf.String(), expected) } func TestSpaces(t *testing.T) { var buf bytes.Buffer var data = [][]string{ {"No", "Age", " City"}, {" 1", "25", "New York"}, {"2", "30", "x"}, {" 3", "28", " Lagos"}, } t.Run("Trim", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithDebug(false), tablewriter.WithTrimSpace(tw.On)) table.Header(data[0]) table.Bulk(data[1:]) table.Render() expected := ` ┌────┬─────┬──────────┐ │ NO │ AGE │ CITY │ ├────┼─────┼──────────┤ │ 1 │ 25 │ New York │ │ 2 │ 30 │ x │ │ 3 │ 28 │ Lagos │ └────┴─────┴──────────┘ ` if !visualCheck(t, "UnicodeTableRendering", buf.String(), expected) { t.Log(table.Debug()) } }) t.Run("NoTrim", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithTrimSpace(tw.Off)) table.Header(data[0]) table.Bulk(data[1:]) table.Render() expected := ` ┌──────────┬─────┬────────────┐ │ NO │ AGE │ CITY │ ├──────────┼─────┼────────────┤ │ 1 │ 25 │ New York │ │ 2 │ 30 │ x │ │ 3 │ 28 │ Lagos │ └──────────┴─────┴────────────┘ ` visualCheck(t, "UnicodeTableRendering", buf.String(), expected) }) } func TestControl(t *testing.T) { var buf bytes.Buffer var data = [][]string{ {"No", "Age", " City"}, {" 1", "25", "New York"}, {"2", "30", "x"}, {" 3", "28", " Lagos"}, } t.Run("Trim", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithDebug(false), tablewriter.WithTrimSpace(tw.On), tablewriter.WithHeaderControl(tw.Control{Hide: tw.On}), ) table.Header(data[0]) table.Bulk(data[1:]) table.Render() expected := ` ┌───┬────┬──────────┐ │ 1 │ 25 │ New York │ │ 2 │ 30 │ x │ │ 3 │ 28 │ Lagos │ └───┴────┴──────────┘ ` if !visualCheck(t, "UnicodeTableRendering", buf.String(), expected) { t.Log(table.Debug()) } }) t.Run("NoTrim", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithTrimSpace(tw.On), tablewriter.WithHeaderControl(tw.Control{Hide: tw.Off}), ) table.Header(data[0]) table.Bulk(data[1:]) table.Render() expected := ` ┌────┬─────┬──────────┐ │ NO │ AGE │ CITY │ ├────┼─────┼──────────┤ │ 1 │ 25 │ New York │ │ 2 │ 30 │ x │ │ 3 │ 28 │ Lagos │ └────┴─────┴──────────┘ ` visualCheck(t, "UnicodeTableRendering", buf.String(), expected) }) } tablewriter-1.0.9/tests/feature_test.go000066400000000000000000000252731504170215200202360ustar00rootroot00000000000000package tests import ( "bytes" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "testing" ) func TestBug260(t *testing.T) { var buf bytes.Buffer tableRendition := tw.Rendition{ Borders: tw.BorderNone, Settings: tw.Settings{ Separators: tw.Separators{ ShowHeader: tw.Off, ShowFooter: tw.Off, BetweenRows: tw.Off, BetweenColumns: tw.Off, }, }, Symbols: tw.NewSymbols(tw.StyleNone), } t.Run("Normal", func(t *testing.T) { buf.Reset() tableRenderer := renderer.NewBlueprint(tableRendition) table := tablewriter.NewTable( &buf, tablewriter.WithRenderer(tableRenderer), tablewriter.WithTableMax(120), tablewriter.WithTrimSpace(tw.Off), tablewriter.WithDebug(true), tablewriter.WithPadding(tw.PaddingNone), ) table.Append([]string{"INFO:", "The original machine had a base-plate of prefabulated aluminite, surmounted by a malleable logarithmic casing in such a way that the two main spurving bearings were in a direct line with the pentametric fan.", }) table.Append("INFO:", "The original machine had a base-plate of prefabulated aluminite, surmounted by a malleable logarithmic casing in such a way that the two main spurving bearings were in a direct line with the pentametric fan.", ) table.Render() expected := ` INFO:The original machine had a base-plate of prefabulated aluminite, surmounted by a malleable logarithmic casing in such a way that the two main spurving bearings were in a direct line with the pentametric fan. INFO:The original machine had a base-plate of prefabulated aluminite, surmounted by a malleable logarithmic casing in such a way that the two main spurving bearings were in a direct line with the pentametric fan. ` debug := visualCheck(t, "TestBug252-Mixed", buf.String(), expected) if !debug { t.Error(table.Debug()) } }) t.Run("Mixed", func(t *testing.T) { buf.Reset() tableRenderer := renderer.NewBlueprint(tableRendition) table := tablewriter.NewTable( &buf, tablewriter.WithRenderer(tableRenderer), tablewriter.WithTableMax(120), tablewriter.WithTrimSpace(tw.Off), tablewriter.WithDebug(true), tablewriter.WithPadding(tw.PaddingNone), ) table.Append([]string{"INFO:", "The original machine had a base-plate of prefabulated aluminite, surmounted by a malleable logarithmic casing in such a way that the two main spurving bearings were in a direct line with the pentametric fan.", }) table.Append("INFO: ", "The original machine had a base-plate of prefabulated aluminite, surmounted by a malleable logarithmic casing in such a way that the two main spurving bearings were in a direct line with the pentametric fan.", ) table.Render() expected := ` INFO: The original machine had a base-plate of prefabulated aluminite, surmounted by a malleable logarithmic casing in such a way that the two main spurving bearings were in a direct line with the pentametric fan. INFO: The original machine had a base-plate of prefabulated aluminite, surmounted by a malleable logarithmic casing in such a way that the two main spurving bearings were in a direct line with the pentametric fan. ` debug := visualCheck(t, "TestBug252-Mixed", buf.String(), expected) if !debug { t.Error(table.Debug()) } }) } func TestBatchPerColumnWidths(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Widths: tw.CellWidth{ PerColumn: tw.NewMapper[int, int]().Set(0, 8).Set(1, 10).Set(2, 15), // Total widths: 8, 5, 15 }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapTruncate, // Truncate content to fit }, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenColumns: tw.On, // Separator width = 1 }, }, }))) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice Smith", "25", "New York City"}) table.Append([]string{"Bob Johnson", "30", "Boston"}) table.Render() // Expected widths: // Col 0: 8 (content=6, pad=1+1, sep=1 for next column) // Col 1: 5 (content=3, pad=1+1, sep=1 for next column) // Col 2: 15 (content=13, pad=1+1, no sep at end) expected := ` ┌────────┬──────────┬───────────────┐ │ NAME │ AGE │ CITY │ ├────────┼──────────┼───────────────┤ │ Alic… │ 25 │ New York City │ │ Bob … │ 30 │ Boston │ └────────┴──────────┴───────────────┘ ` if !visualCheck(t, "BatchPerColumnWidths", buf.String(), expected) { t.Error(table.Debug()) } } func TestBatchGlobalWidthScaling(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Widths: tw.CellWidth{ Global: 20, // Total table width, including padding and separators }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapNormal, }, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenColumns: tw.On, // Separator width = 1 }, }, }))) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice Smith", "25", "New York City"}) table.Append([]string{"Bob Johnson", "30", "Boston"}) table.Render() // Expected widths: // Total width = 20, with 2 separators (2x1 = 2) // Available for columns = 20 - 2 = 18 // 3 columns, so each ~6 (18/3), adjusted for padding and separators // Col 0: 6 (content=4, pad=1+1, sep=1) // Col 1: 6 (content=4, pad=1+1, sep=1) // Col 2: 6 (content=4, pad=1+1) expected := ` ┌──────┬─────┬───────┐ │ NAME │ AGE │ CITY │ ├──────┼─────┼───────┤ │ Alic │ 25 │ New Y │ │ Bob │ 30 │ Bosto │ └──────┴─────┴───────┘ ` if !visualCheck(t, "BatchGlobalWidthScaling", buf.String(), expected) { t.Error(table.Debug()) } } func TestBatchWidthsWithHorizontalMerge(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Widths: tw.CellWidth{ PerColumn: tw.NewMapper[int, int]().Set(0, 10).Set(1, 8).Set(2, 8), // Total widths: 10, 8, 8 }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, AutoWrap: tw.WrapTruncate, }, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenColumns: tw.On, // Separator width = 1 BetweenRows: tw.On, }, }, }))) table.Header([]string{"Name", "Status", "Status"}) table.Append([]string{"Alice", "Active", "Active"}) // Should merge Status columns table.Append([]string{"Bob", "Inactive", "Pending"}) // No merge table.Render() // Expected widths: // Col 0: 10 (content=8, pad=1+1, sep=1) // Col 1: 8 (content=6, pad=1+1, sep=1) // Col 2: 8 (content=6, pad=1+1) // Merged Col 1+2: 8 + 8 - 1 (no separator between) = 15 (content=13, pad=1+1) expected := ` ┌──────────┬────────┬────────┐ │ NAME │ STATUS │ STATUS │ ├──────────┼────────┴────────┤ │ Alice │ Active │ ├──────────┼────────┬────────┤ │ Bob │ Inac… │ Pend… │ └──────────┴────────┴────────┘ ` if !visualCheck(t, "BatchWidthsWithHorizontalMerge", buf.String(), expected) { t.Error(table.Debug()) } } func TestWrapBreakWithConstrainedWidthsNoRightPadding(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithTrimSpace(tw.Off), tablewriter.WithHeaderAutoFormat(tw.Off), tablewriter.WithConfig(tablewriter.Config{ Widths: tw.CellWidth{ PerColumn: tw.NewMapper[int, int]().Set(0, 4).Set(1, 4).Set(2, 6).Set(3, 7), }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapBreak, }, Padding: tw.CellPadding{ Global: tw.PaddingNone, }, }, }), ) headers := []string{"a", "b", "c", "d"} table.Header(headers) data := [][]string{ {"aa", "bb", "cc", "dd"}, {"aaa", "bbb", "ccc", "ddd"}, {"aaaa", "bbbb", "cccc", "dddd"}, {"aaaaa", "bbbbb", "ccccc", "ddddd"}, } table.Bulk(data) table.Render() expected := ` ┌────┬────┬──────┬───────┐ │ A │ B │ C │ D │ ├────┼────┼──────┼───────┤ │aa │bb │cc │dd │ │aaa │bbb │ccc │ddd │ │aaaa│bbbb│cccc │dddd │ │aaa↩│bbb↩│ccccc │ddddd │ │aa │bb │ │ │ └────┴────┴──────┴───────┘ ` if !visualCheck(t, "WrapBreakWithConstrainedWidthsNoRightPadding", buf.String(), expected) { t.Error(table.Debug()) } } func TestCompatMode(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}}, Behavior: tw.Behavior{Compact: tw.Compact{Merge: tw.On}}, Debug: true, })) x := "This is a long header that makes the table look too wide" table.Header([]string{x, x}) table.Append([]string{"Key", "Value"}) table.Render() expected := ` ┌──────────────────────────────────────────────────────────┐ │ THIS IS A LONG HEADER THAT MAKES THE TABLE LOOK TOO WIDE │ ├────────────────────────────┬─────────────────────────────┤ │ Key │ Value │ └────────────────────────────┴─────────────────────────────┘ ` if !visualCheck(t, "TestCompatMode", buf.String(), expected) { t.Error(table.Debug()) } } tablewriter-1.0.9/tests/fn.go000066400000000000000000000232331504170215200161410ustar00rootroot00000000000000package tests import ( "bytes" "encoding/json" "fmt" "regexp" "strings" "testing" "unicode" ) // mismatch represents a discrepancy between expected and actual output lines in a test. type mismatch struct { Line int `json:"line"` // Line number (1-based) Expected string `json:"expected"` // Expected line content and length Got string `json:"got"` // Actual line content and length } // MaskEmail masks email addresses in a slice of strings, replacing all but the first character of the local part with asterisks. func MaskEmail(cells []string) []string { for i, cell := range cells { if strings.Contains(cell, "@") { parts := strings.Split(cell, "@") if len(parts) == 2 { masked := parts[0][:1] + strings.Repeat("*", len(parts[0])-1) + "@" + parts[1] cells[i] = masked } } } return cells } // MaskPassword masks strings that resemble passwords (containing "pass" or 8+ characters) with asterisks. func MaskPassword(cells []string) []string { for i, cell := range cells { if len(cell) > 0 && (strings.Contains(strings.ToLower(cell), "pass") || len(cell) >= 8) { cells[i] = strings.Repeat("*", len(cell)) } } return cells } // MaskCard masks credit card-like numbers, keeping only the last four digits visible. func MaskCard(cells []string) []string { for i, cell := range cells { // Check for card-like numbers (12+ digits, with or without dashes/spaces) if len(cell) >= 12 && (strings.Contains(cell, "-") || len(strings.ReplaceAll(cell, " ", "")) >= 12) { parts := strings.FieldsFunc(cell, func(r rune) bool { return r == '-' || r == ' ' }) masked := "" for j, part := range parts { if j < len(parts)-1 { masked += strings.Repeat("*", len(part)) } else { masked += part // Keep last 4 digits visible } if j < len(parts)-1 { masked += "-" } } cells[i] = masked } } return cells } // visualCheck compares rendered output against expected lines, reporting mismatches in a test. // It normalizes line endings, strips ANSI colors, and trims empty lines before comparison. func visualCheck(t *testing.T, name string, output string, expected string) bool { t.Helper() // Normalize line endings and split into lines normalize := func(s string) []string { s = strings.ReplaceAll(s, "\r\n", "\n") s = StripColors(s) return strings.Split(s, "\n") } expectedLines := normalize(expected) outputLines := normalize(output) // Trim empty lines from start and end trimEmpty := func(lines []string) []string { start, end := 0, len(lines) for start < end && strings.TrimSpace(lines[start]) == "" { start++ } for end > start && strings.TrimSpace(lines[end-1]) == "" { end-- } return lines[start:end] } expectedLines = trimEmpty(expectedLines) outputLines = trimEmpty(outputLines) // Check line counts if len(outputLines) != len(expectedLines) { ex := strings.Join(expectedLines, "\n") ot := strings.Join(outputLines, "\n") t.Errorf("%s: line count mismatch - expected %d, got %d", name, len(expectedLines), len(outputLines)) t.Errorf("Expected:\n%s\n", ex) t.Errorf("Got:\n%s\n", ot) return false } var mismatches []mismatch for i := 0; i < len(expectedLines) && i < len(outputLines); i++ { exp := strings.TrimSpace(expectedLines[i]) got := strings.TrimSpace(outputLines[i]) if exp != got { mismatches = append(mismatches, mismatch{ Line: i + 1, Expected: fmt.Sprintf("%s (%d)", exp, len(exp)), Got: fmt.Sprintf("%s (%d)", got, len(got)), }) } } // Report mismatches if len(mismatches) > 0 { diff, _ := json.MarshalIndent(mismatches, "", " ") t.Errorf("%s: %d mismatches found:\n%s", name, len(mismatches), diff) t.Errorf("Full expected output:\n%s", expected) t.Errorf("Full actual output:\n%s", output) return false } return true } // visualCheckHTML compares rendered HTML output against expected lines, // trimming whitespace per line and ignoring blank lines. func visualCheckHTML(t *testing.T, name string, output string, expected string) bool { t.Helper() normalizeHTML := func(s string) []string { s = strings.ReplaceAll(s, "\r\n", "\n") // Normalize line endings lines := strings.Split(s, "\n") trimmedLines := make([]string, 0, len(lines)) for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed != "" { // Only keep non-blank lines trimmedLines = append(trimmedLines, trimmed) } } return trimmedLines } expectedLines := normalizeHTML(expected) outputLines := normalizeHTML(output) // Compare line counts if len(outputLines) != len(expectedLines) { t.Errorf("%s: line count mismatch - expected %d, got %d", name, len(expectedLines), len(outputLines)) t.Errorf("Expected (trimmed):\n%s", strings.Join(expectedLines, "\n")) t.Errorf("Got (trimmed):\n%s", strings.Join(outputLines, "\n")) // Optionally print full untrimmed for debugging exact whitespace // t.Errorf("Full Expected:\n%s", expected) // t.Errorf("Full Got:\n%s", output) return false } // Compare each line mismatches := []mismatch{} // Use mismatch struct from fn.go for i := 0; i < len(expectedLines); i++ { if expectedLines[i] != outputLines[i] { mismatches = append(mismatches, mismatch{ Line: i + 1, Expected: expectedLines[i], Got: outputLines[i], }) } } if len(mismatches) > 0 { t.Errorf("%s: %d mismatches found:", name, len(mismatches)) for _, mm := range mismatches { t.Errorf(" Line %d:\n Expected: %s\n Got: %s", mm.Line, mm.Expected, mm.Got) } // Optionally print full outputs again on mismatch // t.Errorf("Full Expected:\n%s", expected) // t.Errorf("Full Got:\n%s", output) return false } return true } // ansiColorRegex matches ANSI color escape sequences. var ansiColorRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`) // StripColors removes ANSI color codes from a string. func StripColors(s string) string { return ansiColorRegex.ReplaceAllString(s, "") } // Regex to remove leading/trailing whitespace from lines AND blank lines for HTML comparison var htmlWhitespaceRegex = regexp.MustCompile(`(?m)^\s+|\s+$`) var blankLineRegex = regexp.MustCompile(`(?m)^\s*\n`) func normalizeHTMLStrict(s string) string { s = strings.ReplaceAll(s, "\r\n", "\n") s = strings.ReplaceAll(s, "\n", "") // Remove all newlines s = strings.ReplaceAll(s, "\t", "") // Remove tabs // Remove spaces after > and before <, effectively compacting tags s = regexp.MustCompile(`>\s+`).ReplaceAllString(s, ">") s = regexp.MustCompile(`\s+<`).ReplaceAllString(s, "<") // Trim overall leading/trailing space that might be left. return strings.TrimSpace(s) } // visualCheckCaption (helper function, potentially shared or adapted from your existing visualCheck) // Ensure this helper normalizes expected and got strings for reliable comparison // (e.g., trim spaces from each line, normalize newlines) func visualCheckCaption(t *testing.T, testName, got, expected string) bool { t.Helper() normalize := func(s string) string { s = strings.ReplaceAll(s, "\r\n", "\n") // Normalize newlines lines := strings.Split(s, "\n") var trimmedLines []string for _, l := range lines { trimmedLines = append(trimmedLines, strings.TrimSpace(l)) } // Join, then trim overall to handle cases where expected might have leading/trailing blank lines // but individual lines should keep their relative structure. return strings.TrimSpace(strings.Join(trimmedLines, "\n")) } gotNormalized := normalize(got) expectedNormalized := normalize(expected) if gotNormalized != expectedNormalized { // Use a more detailed diff output if available, or just print both. t.Errorf("%s: outputs do not match.\nExpected:\n```\n%s\n```\nGot:\n```\n%s\n```\n---Diff---\n%s", testName, expected, got, getDiff(expectedNormalized, gotNormalized)) // You might need a diff utility return false } return true } // A simple diff helper (replace with a proper library if needed) func getDiff(expected, actual string) string { expectedLines := strings.Split(expected, "\n") actualLines := strings.Split(actual, "\n") maxLen := len(expectedLines) if len(actualLines) > maxLen { maxLen = len(actualLines) } var diff strings.Builder diff.WriteString("Line | Expected | Actual\n") diff.WriteString("-----|----------------------------------|----------------------------------\n") for i := 0; i < maxLen; i++ { eLine := "" if i < len(expectedLines) { eLine = expectedLines[i] } aLine := "" if i < len(actualLines) { aLine = actualLines[i] } marker := " " if eLine != aLine { marker = "!" } diff.WriteString(fmt.Sprintf("%4d %s| %-32s | %-32s\n", i+1, marker, eLine, aLine)) } return diff.String() } func getLastContentLine(buf *bytes.Buffer) string { content := buf.String() lines := strings.Split(content, "\n") // Search backwards for first non-border, non-empty line for i := len(lines) - 1; i >= 0; i-- { line := strings.TrimSpace(lines[i]) if line == "" || strings.Contains(line, "─") || strings.Contains(line, "┌") || strings.Contains(line, "└") { continue } return line } return "" } type Name struct { First string Last string } // this will be ignored since Format() is present func (n Name) String() string { return fmt.Sprintf("%s %s", n.First, n.Last) } // Note: Format() overrides String() if both exist. func (n Name) Format() string { return fmt.Sprintf("%s %s", clean(n.First), clean(n.Last)) } // clean ensures the first letter is capitalized and the rest are lowercase func clean(s string) string { s = strings.TrimSpace(strings.ToLower(s)) words := strings.Fields(s) s = strings.Join(words, "") if s == "" { return s } // Capitalize the first letter runes := []rune(s) runes[0] = unicode.ToUpper(runes[0]) return string(runes) } tablewriter-1.0.9/tests/html_test.go000066400000000000000000000365371504170215200175540ustar00rootroot00000000000000package tests import ( "bytes" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) // TestHTMLBasicTable verifies that a basic HTML table with headers and rows is rendered correctly. func TestHTMLBasicTable(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := `
NAMEAGECITY
Alice25New York
Bob30Boston
` if !visualCheckHTML(t, "HTMLBasicTable", buf.String(), expected) { t.Error(table.Debug()) } } // TestHTMLWithFooterAndAlignment tests an HTML table with a footer and custom column alignments. func TestHTMLWithFooterAndAlignment(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), tablewriter.WithHeaderConfig(tw.CellConfig{ Formatting: tw.CellFormatting{ AutoFormat: tw.Off, }, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }), tablewriter.WithRowConfig(tw.CellConfig{ Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignLeft, tw.AlignRight, tw.AlignCenter}}, }), tablewriter.WithFooterConfig(tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignRight}, }), ) table.Header([]string{"Item", "Qty", "Price"}) table.Append([]string{"Apple", "5", "1.20"}) table.Append([]string{"Banana", "12", "0.35"}) table.Footer([]string{"", "Total", "7.20"}) table.Render() expected := `
ItemQtyPrice
Apple51.20
Banana120.35
Total7.20
` if !visualCheckHTML(t, "HTMLWithFooterAndAlignment", buf.String(), expected) { t.Error(table.Debug()) } } // TestHTMLEscaping verifies HTML content escaping behavior with and without EscapeContent enabled. func TestHTMLEscaping(t *testing.T) { // Test case 1: Default (EscapeContent = true) var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), ) table.Header([]string{"Tag", "Attribute"}) table.Append([]string{"
", "should escape < & >"}) table.Render() expectedEscaped := `
TAGATTRIBUTE
<br>should escape < & >
` if !visualCheckHTML(t, "HTMLEscaping_Default", buf.String(), expectedEscaped) { t.Log("--- Debug Log for HTMLEscaping_Default ---") t.Log(table.Debug()) } // Test case 2: EscapeContent = false buf.Reset() tableNoEscape := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML(renderer.HTMLConfig{EscapeContent: false})), ) tableNoEscape.Header([]string{"Tag", "Attribute"}) tableNoEscape.Append([]string{"
", "should NOT escape < & >"}) tableNoEscape.Render() expectedUnescaped := `
TAGATTRIBUTE

should NOT escape < & >
` if !visualCheckHTML(t, "HTMLEscaping_Disabled", buf.String(), expectedUnescaped) { t.Log("--- Debug Log for HTMLEscaping_Disabled ---") t.Log(table.Debug()) } } // TestHTMLMultiLine tests HTML table rendering with multiline cell content, noting that newlines create separate rows. func TestHTMLMultiLine(t *testing.T) { // Test case 1: Default behavior (newlines split into separate rows) var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), ) table.Append([]string{"Line 1\nLine 2", "Single Line"}) table.Render() expected := `
Line 1Single Line
Line 2
` if !visualCheckHTML(t, "HTMLMultiLine_Default", buf.String(), expected) { t.Logf("MultiLine Default Output: %s", buf.String()) t.Error(table.Debug()) } // Test case 2: With AddLinesTag (no effect due to newline pre-splitting) buf.Reset() tableLinesTag := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML(renderer.HTMLConfig{AddLinesTag: true})), ) tableLinesTag.Append([]string{"Line 1\nLine 2", "Single Line"}) tableLinesTag.Render() expectedLinesTag := `
Line 1Single Line
Line 2
` if !visualCheckHTML(t, "HTMLMultiLine_LinesTag", buf.String(), expectedLinesTag) { t.Logf("MultiLine LinesTag Output: %s", buf.String()) t.Log(table.Debug()) } } // TestHTMLHorizontalMerge verifies HTML table rendering with horizontal cell merges in headers, rows, and footers. func TestHTMLHorizontalMerge(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}}, Row: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}}, Footer: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}}, }), ) table.Header([]string{"A", "Merged Header", "Merged Header"}) table.Append([]string{"Data 1", "Data 2", "Data 2"}) table.Append([]string{"Merged Row", "Merged Row", "Data 3"}) table.Footer([]string{"Footer 1", "Merged Footer", "Merged Footer"}) table.Render() expected := `
AMERGED HEADER
Data 1Data 2
Merged RowData 3
Footer 1Merged Footer
` if !visualCheckHTML(t, "HTMLHorizontalMerge", buf.String(), expected) { t.Error(table.Debug()) } } // TestHTMLVerticalMerge tests HTML table rendering with vertical cell merges based on repeated values. func TestHTMLVerticalMerge(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeVertical}}, }), ) table.Header([]string{"Category", "Item", "Value"}) table.Append([]string{"Fruit", "Apple", "10"}) table.Append([]string{"Fruit", "Banana", "5"}) table.Append([]string{"Fruit", "Orange", "8"}) table.Append([]string{"Dairy", "Milk", "2"}) table.Append([]string{"Dairy", "Cheese", "4"}) table.Append([]string{"Other", "Bread", "3"}) table.Render() expected := `
CATEGORYITEMVALUE
FruitApple10
Banana5
Orange8
DairyMilk2
Cheese4
OtherBread3
` if !visualCheckHTML(t, "HTMLVerticalMerge", buf.String(), expected) { t.Error(table.Debug()) } } // TestHTMLCombinedMerge verifies HTML table rendering with both horizontal and vertical cell merges. func TestHTMLCombinedMerge(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeBoth}}, }), ) table.Header([]string{"Region", "Quarter", "Sales", "Target"}) table.Append([]string{"North", "Q1", "1000", "900"}) table.Append([]string{"North", "Q2", "1200", "1100"}) table.Append([]string{"South", "Q1+Q2", "Q1+Q2", "2000"}) table.Append([]string{"East", "Q1", "800", "850"}) table.Append([]string{"East", "Q2", "950", "850"}) table.Render() expected := `
REGIONQUARTERSALESTARGET
NorthQ11000900
Q212001100
SouthQ1+Q22000
EastQ1800850
Q2950
` if !visualCheckHTML(t, "HTMLCombinedMerge", buf.String(), expected) { t.Logf("Combined Merge Output: %s", buf.String()) t.Error(table.Debug()) } } // TestHTMLHierarchicalMerge tests HTML table rendering with hierarchical cell merges. func TestHTMLHierarchicalMerge(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical}}, }), tablewriter.WithHeaderConfig(tw.CellConfig{ Formatting: tw.CellFormatting{ AutoFormat: tw.Off, }, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }), ) table.Header([]string{"L1", "L2", "L3"}) table.Append([]string{"A", "a", "1"}) table.Append([]string{"A", "b", "2"}) table.Append([]string{"A", "b", "3"}) table.Append([]string{"B", "c", "4"}) table.Render() expected := `
L1L2L3
Aa1
b2
3
Bc4
` if !visualCheckHTML(t, "HTMLHierarchicalMerge", buf.String(), expected) { t.Error(table.Debug()) } } // TestHTMLEmptyTable verifies HTML rendering for empty tables and tables with only headers. func TestHTMLEmptyTable(t *testing.T) { // Test case 1: Completely empty table var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), ) table.Render() expected := `
` if !visualCheckHTML(t, "HTMLEmptyTable", buf.String(), expected) { t.Logf("Empty table output: '%s'", buf.String()) t.Error(table.Debug()) } // Test case 2: Header-only table buf.Reset() tableHeaderOnly := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), ) tableHeaderOnly.Header([]string{"Col A"}) tableHeaderOnly.Render() expectedHeaderOnly := `
COL A
` if !visualCheckHTML(t, "HTMLEmptyTable_HeaderOnly", buf.String(), expectedHeaderOnly) { t.Log(table.Debug()) } } // TestHTMLCSSClasses tests HTML table rendering with custom CSS classes for table, sections, and rows. func TestHTMLCSSClasses(t *testing.T) { var buf bytes.Buffer htmlCfg := renderer.HTMLConfig{ TableClass: "my-table", HeaderClass: "my-thead", BodyClass: "my-tbody", FooterClass: "my-tfoot", RowClass: "my-row", HeaderRowClass: "my-header-row", FooterRowClass: "my-footer-row", } table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML(htmlCfg)), tablewriter.WithHeaderConfig(tw.CellConfig{ Formatting: tw.CellFormatting{AutoFormat: tw.Off}, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }), ) table.Header([]string{"H1"}) table.Append([]string{"R1"}) table.Footer([]string{"F1"}) table.Render() expected := `
H1
R1
` if !visualCheckHTML(t, "HTMLCSSClasses", buf.String(), expected) { t.Error(table.Debug()) } } // TestHTMLStructureStrict verifies the exact HTML structure of a table without whitespace or formatting variations. func TestHTMLStructureStrict(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewHTML()), ) table.Header([]string{"A", "B"}) table.Append([]string{"1", "2"}) table.Append([]string{"3", "4"}) table.Footer([]string{"F1", "F2"}) table.Render() expectedStructure := `
AB
12
34
F1F2
` outputNormalized := normalizeHTMLStrict(buf.String()) if outputNormalized != expectedStructure { t.Errorf("HTMLStructureStrict: Mismatch") t.Errorf("Expected Structure:\n%s", expectedStructure) t.Errorf("Got Normalized Output:\n%s", outputNormalized) t.Errorf("Got Raw Output:\n%s", buf.String()) } } tablewriter-1.0.9/tests/markdown_test.go000066400000000000000000000176261504170215200204300ustar00rootroot00000000000000package tests import ( "bytes" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) func TestMarkdownBasicTable(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewMarkdown()), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` | NAME | AGE | CITY | |:-----:|:---:|:--------:| | Alice | 25 | New York | | Bob | 30 | Boston | ` if !visualCheck(t, "MarkdownBasicTable", buf.String(), expected) { t.Error(table.Debug()) } } func TestMarkdownAlignment(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewMarkdown()), tablewriter.WithConfig(tablewriter.Config{Header: tw.CellConfig{ Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignLeft, tw.AlignRight, tw.AlignRight, tw.AlignCenter}}}, }), ) table.Header([]string{"Name", "Age", "City", "Status"}) table.Append([]string{"Alice", "25", "New York", "OK"}) table.Append([]string{"Bob", "30", "Boston", "ERROR"}) table.Render() expected := ` | NAME | AGE | CITY | STATUS | |:------|----:|---------:|:------:| | Alice | 25 | New York | OK | | Bob | 30 | Boston | ERROR | ` if !visualCheck(t, "MarkdownBasicTable", buf.String(), expected) { t.Error(table.Debug()) } } func TestMarkdownNoBorders(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewMarkdown(tw.Rendition{ Borders: tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off}, })), tablewriter.WithConfig(tablewriter.Config{Header: tw.CellConfig{ Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignLeft}}}, }), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` NAME | AGE | CITY :------|:---:|:--------: Alice | 25 | New York Bob | 30 | Boston ` visualCheck(t, "MarkdownNoBorders", buf.String(), expected) } func TestMarkdownUnicode(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewMarkdown()), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Bøb", "30", "Tōkyō"}) table.Append([]string{"José", "28", "México"}) table.Append([]string{"张三", "35", "北京"}) table.Render() expected := ` | NAME | AGE | CITY | |:----:|:---:|:------:| | Bøb | 30 | Tōkyō | | José | 28 | México | | 张三 | 35 | 北京 | ` visualCheck(t, "MarkdownUnicode", buf.String(), expected) } func TestMarkdownLongHeaders(t *testing.T) { var buf bytes.Buffer c := tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapTruncate, }, ColMaxWidths: tw.CellWidth{Global: 20}, }, } table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c), tablewriter.WithRenderer(renderer.NewMarkdown()), tablewriter.WithAlignment(tw.MakeAlign(3, tw.AlignLeft)), ) table.Header([]string{"Name", "Age", "Very Long Header That Needs Truncation"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` | NAME | AGE | VERY LONG HEADER… | |:------|:----|:------------------| | Alice | 25 | New York | | Bob | 30 | Boston | ` visualCheck(t, "MarkdownLongHeaders", buf.String(), expected) } func TestMarkdownLongValues(t *testing.T) { var buf bytes.Buffer c := tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoWrap: tw.WrapNormal, }, Alignment: tw.CellAlignment{Global: tw.AlignLeft}, ColMaxWidths: tw.CellWidth{Global: 20}, }, } table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c), tablewriter.WithRenderer(renderer.NewMarkdown()), tablewriter.WithAlignment(tw.MakeAlign(3, tw.AlignLeft)), ) table.Header([]string{"No", "Description", "Note"}) table.Append([]string{"1", "This is a very long description that should wrap", "Short"}) table.Append([]string{"2", "Short desc", "Another note"}) table.Render() expected := ` | NO | DESCRIPTION | NOTE | |:---|:-----------------|:-------------| | 1 | This is a very | Short | | | long description | | | | that should wrap | | | 2 | Short desc | Another note | ` visualCheck(t, "MarkdownLongValues", buf.String(), expected) } func TestMarkdownCustomPadding(t *testing.T) { var buf bytes.Buffer c := tablewriter.Config{ Header: tw.CellConfig{ Padding: tw.CellPadding{ Global: tw.Padding{Left: "*", Right: "*", Top: "", Bottom: ""}, }, }, Row: tw.CellConfig{ Padding: tw.CellPadding{ Global: tw.Padding{Left: ">", Right: "<", Top: "", Bottom: ""}, }, }, } table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c), tablewriter.WithRenderer(renderer.NewMarkdown()), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` |*NAME**|*AGE*|***CITY***| |:-----:|:---:|:--------:| |>Alice<|>25<<|>New York<| |>>Bob<<|>30<<|>>Boston<<| ` visualCheck(t, "MarkdownCustomPadding", buf.String(), expected) } func TestMarkdownHorizontalMerge(t *testing.T) { var buf bytes.Buffer c := tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, }, } table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c), tablewriter.WithRenderer(renderer.NewMarkdown()), ) table.Header([]string{"Merged", "Merged", "Normal"}) table.Append([]string{"Same", "Same", "Unique"}) table.Render() expected := ` | MERGED | NORMAL | |:---------------:|:------:| | Same | Unique | ` visualCheck(t, "MarkdownHorizontalMerge", buf.String(), expected) } func TestMarkdownEmptyTable(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewMarkdown()), ) table.Render() expected := "" visualCheck(t, "MarkdownEmptyTable", buf.String(), expected) } func TestMarkdownWithFooter(t *testing.T) { var buf bytes.Buffer c := tablewriter.Config{ Footer: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignRight}, }, } table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c), tablewriter.WithRenderer(renderer.NewMarkdown()), ) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Footer([]string{"Total", "2", ""}) table.Render() expected := ` | NAME | AGE | CITY | |:-----:|:---:|:--------:| | Alice | 25 | New York | | Bob | 30 | Boston | | Total | 2 | | ` visualCheck(t, "MarkdownWithFooter", buf.String(), expected) } func TestMarkdownAlignmentNone(t *testing.T) { t.Run("AlignNone", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewMarkdown())) table.Configure(func(cfg *tablewriter.Config) { cfg.Header.Alignment.PerColumn = []tw.Align{tw.AlignNone} cfg.Row.Alignment.PerColumn = []tw.Align{tw.AlignNone} cfg.Debug = true }) table.Header([]string{"Header"}) table.Append([]string{"Data"}) table.Render() expected := ` | HEADER | |--------| | Data | ` if !visualCheck(t, "AlignNone", buf.String(), expected) { t.Fatal(table.Debug()) } }) } tablewriter-1.0.9/tests/merge_test.go000066400000000000000000000777321504170215200177110ustar00rootroot00000000000000package tests import ( "bytes" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "testing" ) func TestVerticalMerge(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeVertical, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignRight}}, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.Off, }, }, })), ) table.Header([]string{"Name", "Sign", "Rating"}) table.Append([]string{"A", "The Good", "500"}) table.Append([]string{"A", "The Very very Bad Man", "288"}) table.Append([]string{"B", "", "120"}) table.Append([]string{"B", "", "200"}) table.Render() expected := ` ┌──────┬───────────────────────┬────────┐ │ NAME │ SIGN │ RATING │ ├──────┼───────────────────────┼────────┤ │ A │ The Good │ 500 │ │ │ The Very very Bad Man │ 288 │ │ B │ │ 120 │ │ │ │ 200 │ └──────┴───────────────────────┴────────┘ ` if !visualCheck(t, "VerticalMerge", buf.String(), expected) { t.Error(table.Debug()) } } func TestHorizontalMerge(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignCenter, tw.AlignCenter, tw.AlignCenter, tw.AlignCenter}}, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.Off, }, }, })), ) table.Header([]string{"Col1", "Col2", "Col2"}) table.Append([]string{"A", "B", "B"}) table.Append([]string{"A", "A", "C"}) table.Append([]string{"A", "B", "C"}) table.Append([]string{"B", "C", "C"}) table.Append([]string{"B", "C", "D"}) table.Append([]string{"D", "D", "D"}) table.Render() expected := ` ┌───────┬───────┬───────┐ │ COL 1 │ COL 2 │ COL 2 │ ├───────┼───────┴───────┤ │ A │ B │ │ A │ C │ │ A │ B │ C │ │ B │ C │ │ B │ C │ D │ │ D │ └───────────────────────┘ ` if !visualCheck(t, "TestHorizontalMerge", buf.String(), expected) { t.Error(table.Debug()) } } func TestHorizontalMergeEachLine(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, }, Footer: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}}, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ // Symbols: tw.NewSymbols(tw.StyleMerger), Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, })), ) table.Header([]string{"Date", "Section A", "Section B", "Section C", "Section D", "Section E"}) table.Append([]string{"1/1/2014", "apple", "boy", "cat", "dog", "elephant"}) table.Append([]string{"1/1/2014", "apple", "apple", "boy", "dog", "elephant"}) table.Append([]string{"1/1/2014", "apple", "boy", "boy", "cat", "dog"}) table.Append([]string{"1/1/2014", "apple", "boy", "cat", "cat", "dog"}) table.Render() expected := ` ┌──────────┬───────────┬───────────┬───────────┬───────────┬───────────┐ │ DATE │ SECTION A │ SECTION B │ SECTION C │ SECTION D │ SECTION E │ ├──────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 1/1/2014 │ apple │ boy │ cat │ dog │ elephant │ ├──────────┼───────────┴───────────┼───────────┼───────────┼───────────┤ │ 1/1/2014 │ apple │ boy │ dog │ elephant │ ├──────────┼───────────┬───────────┴───────────┼───────────┼───────────┤ │ 1/1/2014 │ apple │ boy │ cat │ dog │ ├──────────┼───────────┼───────────┬───────────┴───────────┼───────────┤ │ 1/1/2014 │ apple │ boy │ cat │ dog │ └──────────┴───────────┴───────────┴───────────────────────┴───────────┘ ` check := visualCheck(t, "HorizontalMergeEachLine", buf.String(), expected) if !check { t.Error(table.Debug()) } } func TestHorizontalMergeEachLineCenter(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ // Symbols: tw.NewSymbols(tw.StyleMerger), Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, })), ) table.Header([]string{"Date", "Section A", "Section B", "Section C", "Section D", "Section E"}) table.Append([]string{"1/1/2014", "apple", "boy", "cat", "dog", "elephant"}) table.Append([]string{"1/1/2014", "apple", "apple", "boy", "dog", "elephant"}) table.Append([]string{"1/1/2014", "apple", "boy", "boy", "cat", "dog"}) table.Append([]string{"1/1/2014", "apple", "boy", "cat", "cat", "dog"}) table.Render() expected := ` ┌──────────┬───────────┬───────────┬───────────┬───────────┬───────────┐ │ DATE │ SECTION A │ SECTION B │ SECTION C │ SECTION D │ SECTION E │ ├──────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 1/1/2014 │ apple │ boy │ cat │ dog │ elephant │ ├──────────┼───────────┴───────────┼───────────┼───────────┼───────────┤ │ 1/1/2014 │ apple │ boy │ dog │ elephant │ ├──────────┼───────────┬───────────┴───────────┼───────────┼───────────┤ │ 1/1/2014 │ apple │ boy │ cat │ dog │ ├──────────┼───────────┼───────────┬───────────┴───────────┼───────────┤ │ 1/1/2014 │ apple │ boy │ cat │ dog │ └──────────┴───────────┴───────────┴───────────────────────┴───────────┘ ` check := visualCheck(t, "HorizontalMergeEachLineCenter", buf.String(), expected) if !check { t.Error(table.Debug()) } } func TestHorizontalMergeAlignFooter(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, }, Footer: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}}, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, })), ) table.Header([]string{"Date", "Description", "Status", "Conclusion"}) table.Append([]string{"1/1/2014", "Domain name", "Successful", "Successful"}) table.Append([]string{"1/1/2014", "Domain name", "Pending", "Waiting"}) table.Append([]string{"1/1/2014", "Domain name", "Successful", "Rejected"}) table.Footer([]string{"", "", "TOTAL", "$145.93"}) // Fixed from Append table.Render() expected := ` ┌──────────┬─────────────┬────────────┬────────────┐ │ DATE │ DESCRIPTION │ STATUS │ CONCLUSION │ ├──────────┼─────────────┼────────────┴────────────┤ │ 1/1/2014 │ Domain name │ Successful │ ├──────────┼─────────────┼────────────┬────────────┤ │ 1/1/2014 │ Domain name │ Pending │ Waiting │ ├──────────┼─────────────┼────────────┼────────────┤ │ 1/1/2014 │ Domain name │ Successful │ Rejected │ ├──────────┴─────────────┴────────────┼────────────┤ │ TOTAL │ $145.93 │ └─────────────────────────────────────┴────────────┘ ` check := visualCheck(t, "HorizontalMergeAlignFooter", buf.String(), expected) if !check { t.Error(table.Debug()) } } func TestVerticalMergeLines(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeVertical, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignRight}}, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ // Symbols: tw.NewSymbols(tw.StyleMerger), Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, }))) table.Header([]string{"Name", "Sign", "Rating"}) table.Append([]string{"A", "The Good", "500"}) table.Append([]string{"A", "The Very very Bad Man", "288"}) table.Append([]string{"B", "C", "120"}) table.Append([]string{"B", "C", "200"}) table.Render() expected := ` ┌──────┬───────────────────────┬────────┐ │ NAME │ SIGN │ RATING │ ├──────┼───────────────────────┼────────┤ │ A │ The Good │ 500 │ │ ├───────────────────────┼────────┤ │ │ The Very very Bad Man │ 288 │ ├──────┼───────────────────────┼────────┤ │ B │ C │ 120 │ │ │ ├────────┤ │ │ │ 200 │ └──────┴───────────────────────┴────────┘ ` if !visualCheck(t, "VerticalMergeLines", buf.String(), expected) { t.Error(table.Debug()) } } func TestMergeBoth(t *testing.T) { var buf bytes.Buffer c := tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeBoth, }, }, Footer: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignRight, tw.AlignRight, tw.AlignRight, tw.AlignLeft}}, }, } r := renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, }) t.Run("mixed-1", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c), tablewriter.WithRenderer(r)) table.Header([]string{"Date", "Description", "Status", "Conclusion"}) table.Append([]string{"1/1/2014", "Domain name", "Successful", "Successful"}) table.Append([]string{"1/1/2014", "Domain name", "Pending", "Waiting"}) table.Append([]string{"1/1/2014", "Domain name", "Successful", "Rejected"}) table.Footer([]string{"TOTAL", "TOTAL", "TOTAL", "$145.93"}) table.Render() expected := ` ┌──────────┬─────────────┬────────────┬────────────┐ │ DATE │ DESCRIPTION │ STATUS │ CONCLUSION │ ├──────────┼─────────────┼────────────┴────────────┤ │ 1/1/2014 │ Domain name │ Successful │ │ │ ├────────────┬────────────┤ │ │ │ Pending │ Waiting │ │ │ ├────────────┼────────────┤ │ │ │ Successful │ Rejected │ ├──────────┴─────────────┴────────────┼────────────┤ │ TOTAL │ $145.93 │ └─────────────────────────────────────┴────────────┘ ` check := visualCheck(t, "TestMergeBoth-mixed-1", buf.String(), expected) if !check { t.Log(table.Debug()) } }) t.Run("mixed-2", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithConfig(c), tablewriter.WithRenderer(r)) table.Header([]string{"Date", "Description", "Status", "Conclusion"}) table.Append([]string{"1/1/2014", "Domain name", "Successful", "Successful"}) table.Append([]string{"1/1/2014", "Domain name", "Pending", "Waiting"}) table.Append([]string{"1/1/2015", "Domain name", "Successful", "Rejected"}) table.Footer([]string{"TOTAL", "TOTAL", "TOTAL", "$145.93"}) table.Render() expected := ` ┌──────────┬─────────────┬────────────┬────────────┐ │ DATE │ DESCRIPTION │ STATUS │ CONCLUSION │ ├──────────┼─────────────┼────────────┴────────────┤ │ 1/1/2014 │ Domain name │ Successful │ │ │ ├────────────┬────────────┤ │ │ │ Pending │ Waiting │ ├──────────┤ ├────────────┼────────────┤ │ 1/1/2015 │ │ Successful │ Rejected │ ├──────────┴─────────────┴────────────┼────────────┤ │ TOTAL │ $145.93 │ └─────────────────────────────────────┴────────────┘ ` check := visualCheck(t, "TestMergeBoth-mixed-2", buf.String(), expected) if !check { t.Log(table.Debug()) } }) } func TestMergeHierarchical(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHierarchical, }, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleASCII), Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, })), ) table.Header([]string{"0", "1", "2", "3"}) table.Append([]string{"A", "a", "c", "-"}) table.Append([]string{"A", "b", "c", "-"}) table.Append([]string{"A", "b", "d", "-"}) table.Append([]string{"B", "b", "d", "-"}) table.Render() expected := ` +---+---+---+---+ | 0 | 1 | 2 | 3 | +---+---+---+---+ | A | a | c | - | | +---+---+---+ | | b | c | - | | | +---+---+ | | | d | - | +---+---+---+---+ | B | b | d | - | +---+---+---+---+ ` check := visualCheck(t, "MergeHierarchical", buf.String(), expected) if !check { t.Error(table.Debug()) } } func TestMergeHierarchicalUnicode(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHierarchical, }, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ // Symbols: tw.NewSymbols(tw.StyleRounded), Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, })), ) table.Header([]string{"0", "1", "2", "3"}) table.Append([]string{"A", "a", "c", "-"}) table.Append([]string{"A", "b", "c", "-"}) table.Append([]string{"A", "b", "d", "-"}) table.Append([]string{"B", "b", "d", "-"}) table.Render() expected := ` ┌───┬───┬───┬───┐ │ 0 │ 1 │ 2 │ 3 │ ├───┼───┼───┼───┤ │ A │ a │ c │ - │ │ ├───┼───┼───┤ │ │ b │ c │ - │ │ │ ├───┼───┤ │ │ │ d │ - │ ├───┼───┼───┼───┤ │ B │ b │ d │ - │ └───┴───┴───┴───┘ ` check := visualCheck(t, "MergeHierarchicalUnicode", buf.String(), expected) if !check { t.Error(table.Debug()) } } func TestMergeWithPadding(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeBoth, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}}, }, Footer: tw.CellConfig{ Padding: tw.CellPadding{ Global: tw.Padding{Left: "*", Right: "*", Top: "", Bottom: ""}, PerColumn: []tw.Padding{{}, {}, {Bottom: "^"}, {Bottom: "."}}, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}}, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ //Symbols: tw.NewSymbols(tw.StyleASCII), Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, }))) table.Header([]string{"Date", "Description", "Status", "Conclusion"}) table.Append([]string{"1/1/2014", "Domain name", "Successful", "Successful"}) table.Append([]string{"1/1/2014", "Domain name", "Pending", "Waiting"}) table.Append([]string{"1/1/2014", "Domain name", "Successful", "Rejected"}) table.Append([]string{"", "", "TOTAL", "$145.93"}) table.Render() expected := ` ┌──────────┬─────────────┬────────────┬────────────┐ │ DATE │ DESCRIPTION │ STATUS │ CONCLUSION │ ├──────────┼─────────────┼────────────┴────────────┤ │ 1/1/2014 │ Domain name │ Successful │ │ │ ├────────────┬────────────┤ │ │ │ Pending │ Waiting │ │ │ ├────────────┼────────────┤ │ │ │ Successful │ Rejected │ ├──────────┼─────────────┼────────────┼────────────┤ │ │ │ TOTAL │ $145.93 │ │ │ │^^^^^^^^^^^^│............│ └──────────┴─────────────┴────────────┴────────────┘ ` visualCheck(t, "MergeWithPadding", buf.String(), expected) } func TestMergeWithMultipleLines(t *testing.T) { var buf bytes.Buffer data := [][]string{ {"Module", "Description", "Version", "Status"}, {"core\nutils", "Utility\nfunctions", "v1.0.0", "stable"}, {"core\nutils", "Helper\nroutines", "v1.1.0", "beta"}, {"web\nserver", "HTTP\nserver", "v2.0.0", "stable"}, {"web\nserver", "", "v2.1.0", "testing"}, {"db\nclient", "Database\naccess", "v3.0.0", ""}, } t.Run("Horizontal", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignLeft, tw.AlignLeft, tw.AlignLeft, tw.AlignLeft}}, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, }))) table.Header(data[0]) table.Bulk(data[1:]) table.Render() expected := ` ┌────────┬─────────────┬─────────┬─────────┐ │ MODULE │ DESCRIPTION │ VERSION │ STATUS │ ├────────┼─────────────┼─────────┼─────────┤ │ core │ Utility │ v1.0.0 │ stable │ │ utils │ functions │ │ │ ├────────┼─────────────┼─────────┼─────────┤ │ core │ Helper │ v1.1.0 │ beta │ │ utils │ routines │ │ │ ├────────┼─────────────┼─────────┼─────────┤ │ web │ HTTP │ v2.0.0 │ stable │ │ server │ server │ │ │ ├────────┼─────────────┼─────────┼─────────┤ │ web │ │ v2.1.0 │ testing │ │ server │ │ │ │ ├────────┼─────────────┼─────────┼─────────┤ │ db │ Database │ v3.0.0 │ │ │ client │ access │ │ │ └────────┴─────────────┴─────────┴─────────┘ ` // t.Log("====== LOG", table.Logger().Enabled()) if !visualCheck(t, "Horizontal", buf.String(), expected) { t.Error(table.Debug()) } }) t.Run("Vertical", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeVertical, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignLeft, tw.AlignLeft, tw.AlignLeft, tw.AlignLeft}}, }, Debug: true, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, }))) table.Header(data[0]) table.Bulk(data[1:]) table.Render() expected := ` ┌────────┬─────────────┬─────────┬─────────┐ │ MODULE │ DESCRIPTION │ VERSION │ STATUS │ ├────────┼─────────────┼─────────┼─────────┤ │ core │ Utility │ v1.0.0 │ stable │ │ utils │ functions │ │ │ │ ├─────────────┼─────────┼─────────┤ │ │ Helper │ v1.1.0 │ beta │ │ │ routines │ │ │ ├────────┼─────────────┼─────────┼─────────┤ │ web │ HTTP │ v2.0.0 │ stable │ │ server │ server │ │ │ │ ├─────────────┼─────────┼─────────┤ │ │ │ v2.1.0 │ testing │ ├────────┼─────────────┼─────────┼─────────┤ │ db │ Database │ v3.0.0 │ │ │ client │ access │ │ │ └────────┴─────────────┴─────────┴─────────┘ ` if !visualCheck(t, "Vertical", buf.String(), expected) { t.Error(table.Debug()) } }) t.Run("Hierarch", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Formatting: tw.CellFormatting{ MergeMode: tw.MergeHierarchical, }, Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignLeft, tw.AlignLeft, tw.AlignLeft, tw.AlignLeft}}, }, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, }))) table.Header(data[0]) table.Bulk(data[1:]) table.Render() expected := ` ┌────────┬─────────────┬─────────┬─────────┐ │ MODULE │ DESCRIPTION │ VERSION │ STATUS │ ├────────┼─────────────┼─────────┼─────────┤ │ core │ Utility │ v1.0.0 │ stable │ │ utils │ functions │ │ │ │ ├─────────────┼─────────┼─────────┤ │ │ Helper │ v1.1.0 │ beta │ │ │ routines │ │ │ ├────────┼─────────────┼─────────┼─────────┤ │ web │ HTTP │ v2.0.0 │ stable │ │ server │ server │ │ │ │ ├─────────────┼─────────┼─────────┤ │ │ │ v2.1.0 │ testing │ ├────────┼─────────────┼─────────┼─────────┤ │ db │ Database │ v3.0.0 │ │ │ client │ access │ │ │ └────────┴─────────────┴─────────┴─────────┘ ` if !visualCheck(t, "Hierarch", buf.String(), expected) { // t.Error(table.Debug()) } }) } func TestMergeHierarchicalCombined(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, Row: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical}, Alignment: tw.CellAlignment{Global: tw.AlignLeft}, }, Footer: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignLeft}, Formatting: tw.CellFormatting{ MergeMode: tw.MergeHorizontal, }, }, Debug: true, }), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}}, })), ) data := [][]string{ {"Engineering", "Backend", "API Team", "Alice"}, {"Engineering", "Backend", "Database Team", "Bob"}, {"Engineering", "Frontend", "UI Team", "Charlie"}, {"Marketing", "Digital", "SEO Team", "Dave"}, {"Marketing", "Digital", "Content Team", "Eve"}, } table.Header([]string{"Department", "Division", "Team", "Lead"}) table.Bulk(data) table.Footer([]string{"Total Teams", "5", "5", "5"}) table.Render() expected := ` ┌─────────────┬──────────┬───────────────┬─────────┐ │ DEPARTMENT │ DIVISION │ TEAM │ LEAD │ ├─────────────┼──────────┼───────────────┼─────────┤ │ Engineering │ Backend │ API Team │ Alice │ │ │ ├───────────────┼─────────┤ │ │ │ Database Team │ Bob │ │ ├──────────┼───────────────┼─────────┤ │ │ Frontend │ UI Team │ Charlie │ ├─────────────┼──────────┼───────────────┼─────────┤ │ Marketing │ Digital │ SEO Team │ Dave │ │ │ ├───────────────┼─────────┤ │ │ │ Content Team │ Eve │ ├─────────────┼──────────┴───────────────┴─────────┤ │ Total Teams │ 5 │ └─────────────┴────────────────────────────────────┘ ` check := visualCheck(t, "MergeHierarchical", buf.String(), expected) if !check { t.Error(table.Debug()) } } tablewriter-1.0.9/tests/ocean_test.go000066400000000000000000000345661504170215200176750ustar00rootroot00000000000000package tests // Assuming your tests are in a _test package import ( "bytes" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "testing" ) func TestOceanTableDefault(t *testing.T) { // You already have this, keep it var buf bytes.Buffer // Using Ocean renderer in BATCH mode here via table.Render() table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewOcean()), tablewriter.WithDebug(false)) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := ` ┌───────┬─────┬──────────┐ │ NAME │ AGE │ CITY │ ├───────┼─────┼──────────┤ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ └───────┴─────┴──────────┘ ` if !visualCheck(t, "OceanTableRendering_BatchDefault", buf.String(), expected) { t.Error(table.Debug()) } } func TestOceanTableStreaming_Simple(t *testing.T) { var buf bytes.Buffer data := [][]string{ {"Name", "Age", "City"}, {"Alice", "25", "New York"}, {"Bob", "30", "Boston"}, } // Define fixed widths for streaming. Ocean relies on these. // Content + 2 spaces for padding widths := tw.NewMapper[int, int]() widths.Set(0, 4+2) // NAME widths.Set(1, 3+2) // AGE widths.Set(2, 8+2) // New York tbl := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewOcean()), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), tablewriter.WithColumnWidths(widths), ) err := tbl.Start() if err != nil { t.Fatalf("tbl.Start() failed: %v", err) } tbl.Header(data[0]) tbl.Append(data[1]) tbl.Append(data[2]) err = tbl.Close() if err != nil { t.Fatalf("tbl.Close() failed: %v", err) } expected := ` ┌──────┬─────┬──────────┐ │ NAME │ AGE │ CITY │ ├──────┼─────┼──────────┤ │ Alic │ 25 │ New York │ │ Bob │ 30 │ Boston │ └──────┴─────┴──────────┘ ` // Note: Align differences might occur if streaming path doesn't pass full CellContext for alignment // The expected output assumes default (left) alignment for rows, center for header. // Ocean's formatCellContent will apply these based on ctx.Row.Position if no cellCtx.Align. if !visualCheck(t, "OceanTableStreaming_Simple", buf.String(), expected) { t.Error(tbl.Debug().String()) } } func TestOceanTableStreaming_NoHeader(t *testing.T) { var buf bytes.Buffer data := [][]string{ {"Alice", "25", "New York"}, {"Bob", "30", "Boston"}, } widths := tw.NewMapper[int, int]() widths.Set(0, 5+2) // Alice widths.Set(1, 2+2) // 25 widths.Set(2, 8+2) // New York tbl := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewOcean()), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), tablewriter.WithColumnWidths(widths), ) err := tbl.Start() if err != nil { t.Fatalf("tbl.Start() failed: %v", err) } // No tbl.Header() call tbl.Append(data[0]) tbl.Append(data[1]) err = tbl.Close() if err != nil { t.Fatalf("tbl.Close() failed: %v", err) } expected := ` ┌───────┬────┬──────────┐ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ └───────┴────┴──────────┘ ` // If ShowHeaderLine is true (default for Ocean), it should still draw a line // if the table starts directly with rows. This test implicitly checks that. // The default config for Ocean.Settings.Lines.ShowHeaderLine = tw.On // However, if no header content is *ever* processed, and then rows start, // the `stream.go` logic or `Ocean.Row` needs to detect it's the first *actual* content // and draw the top border, and then a line *if* ShowHeaderLine implies a line even for empty headers. // The current Ocean default config has ShowHeaderLine: On. The stream logic needs to call Line() for this. // EXPECTED (if header line IS drawn because ShowHeaderLine is ON even if no header content) // If stream.go or Ocean.Row handles drawing the line before first row when no header. expectedWithHeaderLine := ` ┌───────┬────┬──────────┐ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ └───────┴────┴──────────┘ ` // The prior Ocean code changes made table.go's stream logic responsible for these lines. // Let's assume table.go's stream logic will correctly call ocean.Line() for the header separator // if ShowHeaderLine is true, even if no Ocean.Header() content was called. // The current test framework (TestStreamTableDefault in streamer_test.go) might already cover this. // For this specific Ocean test, we check if Ocean *behaves* correctly when such Line() calls are made. if !visualCheck(t, "OceanTableStreaming_NoHeader_WithHeaderLine", buf.String(), expectedWithHeaderLine) { // If the above fails, it might be that the stream logic in table.go // doesn't call the header separator if Ocean.Header() itself isn't called. // In that case, the expected output would be `expected` (without the internal line). // For now, we'll assume table.go's streaming path correctly instructs Line() for header sep. t.Log("DEBUG LOG for OceanTableStreaming_NoHeader_WithHeaderLine:\n" + tbl.Debug().String()) // Try the alternative if the primary expectation fails t.Logf("Primary expectation (with header line) failed. Trying expectation without header line.") if !visualCheck(t, "OceanTableStreaming_NoHeader_WithoutHeaderLine", buf.String(), expected) { t.Error("Also failed expectation without header line.") } } } func TestOceanTableStreaming_WithFooter(t *testing.T) { var buf bytes.Buffer header := []string{"Item", "Qty"} data := [][]string{ {"Apples", "5"}, {"Pears", "2"}, } footer := []string{"Total", "7"} widths := tw.NewMapper[int, int]() widths.Set(0, 6+2) // Apples widths.Set(1, 3+2) // Qty / 7 // Ocean default: ShowFooterLine = Off // Let's test with it ON for Ocean specifically to see if it renders a line before footer. oceanR := renderer.NewOcean() oceanCfg := oceanR.Config() // Get mutable copy oceanCfg.Settings.Lines.ShowFooterLine = tw.On // (Ideally, NewOcean would take Rendition or there'd be an ApplyConfig method) // For this test, we'll rely on modifying the default or creating a new one if easy // For now, we assume the test setup in tablewriter.go will pass the correct config. // This test will use the default Ocean config for ShowFooterLine (Off). tbl := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewOcean()), // Uses default Ocean config initially tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), tablewriter.WithColumnWidths(widths), ) err := tbl.Start() if err != nil { t.Fatalf("tbl.Start() failed: %v", err) } tbl.Header(header) for _, row := range data { tbl.Append(row) } tbl.Footer(footer) // This should store footer, Close() will trigger render err = tbl.Close() if err != nil { t.Fatalf("tbl.Close() failed: %v", err) } expected := ` ┌────────┬─────┐ │ ITEM │ QTY │ ├────────┼─────┤ │ Apples │ 5 │ │ Pears │ 2 │ │ Total │ 7 │ └────────┴─────┘ ` if !visualCheck(t, "OceanTableStreaming_WithFooter", buf.String(), expected) { t.Error(tbl.Debug().String()) } } func TestOceanTableStreaming_VaryingWidthsFromConfig(t *testing.T) { var buf bytes.Buffer header := []string{"Short", "Medium Header", "This is a Very Long Header"} data := [][]string{ {"A", "Med Data", "Long Data Cell Content"}, {"B", "More Med", "Another Long One"}, } // Widths for content: Short(5), Medium Header(13), Very Long Header(26) // Add padding of 2 (1 left, 1 right) widths := tw.NewMapper[int, int]() widths.Set(0, 5+2) widths.Set(1, 13+2) widths.Set(2, 20+2) // Stream width is 20, content is 26. Expect truncation. tbl := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewOcean()), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), tablewriter.WithColumnWidths(widths), ) err := tbl.Start() if err != nil { t.Fatalf("Start failed: %v", err) } tbl.Header(header) for _, row := range data { tbl.Append(row) } err = tbl.Close() if err != nil { t.Fatalf("Close failed: %v", err) } expected := ` ┌───────┬───────────────┬──────────────────────┐ │ SHORT │ MEDIUM HEADER │ THIS IS A VERY LON… │ ├───────┼───────────────┼──────────────────────┤ │ A │ Med Data │ Long Data Cell │ │ │ │ Content │ │ B │ More Med │ Another Long One │ └───────┴───────────────┴──────────────────────┘ ` // Note: Content like "This is a Very Long Header" (26) + padding (2) = 28. // Stream width for col 2 is 22. Content area = 20. Ellipsis is 1. So, 19 chars + "…" // "This is a Very Long" (19) + "…" = "This is a Very Long…" // "Long Data Cell Content" (24) -> "Long Data Cell Cont…" // "Another Long One" (16) fits. // "A" (1) vs width 7 (content 5). "Med Data" (8) vs width 15 (content 13). if !visualCheck(t, "OceanTableStreaming_VaryingWidths", buf.String(), expected) { t.Error(tbl.Debug().String()) } } func TestOceanTableStreaming_MultiLineCells(t *testing.T) { var buf bytes.Buffer header := []string{"ID", "Description"} data := [][]string{ {"1", "First item\nwith two lines."}, {"2", "Second item\nwhich also has\nthree lines."}, } widths := tw.NewMapper[int, int]() widths.Set(0, 2+2) // ID widths.Set(1, 15+2) // Description (max line "three lines.") tbl := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewOcean()), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), tablewriter.WithColumnWidths(widths), ) err := tbl.Start() if err != nil { t.Fatalf("Start failed: %v", err) } tbl.Header(header) for _, row := range data { tbl.Append(row) } err = tbl.Close() if err != nil { t.Fatalf("Close failed: %v", err) } expected := ` ┌────┬─────────────────┐ │ ID │ DESCRIPTION │ ├────┼─────────────────┤ │ 1 │ First item │ │ │ with two lines. │ │ 2 │ Second item │ │ │ which also has │ │ │ three lines. │ └────┴─────────────────┘ ` if !visualCheck(t, "OceanTableStreaming_MultiLineCells", buf.String(), expected) { t.Error(tbl.Debug().String()) } } func TestOceanTableStreaming_OnlyHeader(t *testing.T) { var buf bytes.Buffer header := []string{"Col A", "Col B"} widths := tw.NewMapper[int, int]() widths.Set(0, 5+2) widths.Set(1, 5+2) tbl := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewOcean()), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), tablewriter.WithColumnWidths(widths), ) err := tbl.Start() if err != nil { t.Fatalf("Start failed: %v", err) } tbl.Header(header) err = tbl.Close() if err != nil { t.Fatalf("Close failed: %v", err) } expected := ` ┌───────┬───────┐ │ COL A │ COL B │ └───────┴───────┘ ` // Expect top border, header, header separator, and bottom border. if !visualCheck(t, "OceanTableStreaming_OnlyHeader", buf.String(), expected) { t.Error(tbl.Debug().String()) } } func TestOceanTableStreaming_HorizontalMerge(t *testing.T) { var buf bytes.Buffer header := []string{"Category", "Value 1", "Value 2"} data := [][]string{ {"Fruit", "Apple", "Red"}, {"Color", "Blue (spans next)", ""}, // "Blue" will span, "" will be ignored for content {"Shape", "Circle", "Round"}, } footer := []string{"Summary", "Total 3 items", ""} widths := tw.NewMapper[int, int]() widths.Set(0, 8+2) // Category/Fruit/Color/Shape/Summary widths.Set(1, 15+2) // Value 1 / Apple / Blue / Circle / Total 3 items widths.Set(2, 5+2) // Value 2 / Red / "" / Round / "" tbl := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewOcean()), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), tablewriter.WithColumnWidths(widths), ) err := tbl.Start() if err != nil { t.Fatalf("Start failed: %v", err) } tbl.Header(header) for _, row := range data { tbl.Append(row) } tbl.Footer(footer) err = tbl.Close() if err != nil { t.Fatalf("Close failed: %v", err) } // For "Blue (spans next)", Value 1 (width 17) + Value 2 (width 7) + Sep (1) = 25 // For "Total 3 items", Value 1 (width 17) + Value 2 (width 7) + Sep (1) = 25 expected := ` ┌──────────┬─────────────────┬───────┐ │ CATEGORY │ VALUE 1 │ VAL… │ ├──────────┼─────────────────┼───────┤ │ Fruit │ Apple │ Red │ │ Color │ Blue (spans │ │ │ │ next) │ │ │ Shape │ Circle │ Round │ │ Summary │ Total 3 items │ │ └──────────┴─────────────────┴───────┘ ` if !visualCheck(t, "OceanTableStreaming_HorizontalMerge", buf.String(), expected) { t.Error(tbl.Debug().String()) } } tablewriter-1.0.9/tests/streamer_test.go000066400000000000000000000471471504170215200204310ustar00rootroot00000000000000package tests import ( "bytes" "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "strings" "testing" "time" ) // createStreamTable creates a TableStream with the renderer for testing. func createStreamTable(t *testing.T, w *bytes.Buffer, opts ...tablewriter.Option) *tablewriter.Table { t.Helper() opts = append(opts, tablewriter.WithRenderer(renderer.NewBlueprint())) return tablewriter.NewTable(w, opts...) } func TestStreamTableDefault(t *testing.T) { var buf bytes.Buffer t.Run("disabled", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithStreaming(tw.StreamConfig{Enable: false})) //err := table.Start() //if err != nil { // t.Fatalf("Start failed: %v", err) //} table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) //err = table.Close() //if err != nil { // t.Fatalf("End failed: %v", err) //} table.Render() expected := ` ┌───────┬─────┬──────────┐ │ NAME │ AGE │ CITY │ ├───────┼─────┼──────────┤ │ Alice │ 25 │ New York │ │ Bob │ 30 │ Boston │ └───────┴─────┴──────────┘ ` debug := visualCheck(t, "TestStreamTableDefault", buf.String(), expected) if !debug { t.Error(table.Debug()) } }) t.Run("enabled", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable(&buf, tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), tablewriter.WithDebug(false)) err := table.Start() if err != nil { t.Fatalf("Start failed: %v", err) } table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) err = table.Close() if err != nil { t.Fatalf("End failed: %v", err) } expected := ` ┌────────┬────────┬────────┐ │ NAME │ AGE │ CITY │ ├────────┼────────┼────────┤ │ Alice │ 25 │ New │ │ │ │ York │ │ Bob │ 30 │ Boston │ └────────┴────────┴────────┘ ` debug := visualCheck(t, "BasicTableRendering", buf.String(), expected) if !debug { t.Error(table.Debug()) } }) } // TestStreamBasic tests basic streaming table rendering with header, rows, and footer. func TestStreamBasic(t *testing.T) { var buf bytes.Buffer t.Run("TestStreamBasic", func(t *testing.T) { buf.Reset() st := createStreamTable(t, &buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}}, Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}}}), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), ) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } st.Header([]string{"Name", "Age", "City"}) st.Append([]string{"Alice", "25", "New York"}) st.Append([]string{"Bob", "30", "Boston"}) st.Footer([]string{"Total", "55", "*"}) err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } // Widths: Name(5)+2=7, Age(3)+2=5, City(8)+2=10 expected := ` ┌────────┬────────┬────────┐ │ NAME │ AGE │ CITY │ ├────────┼────────┼────────┤ │ Alice │ 25 │ New │ │ │ │ York │ │ Bob │ 30 │ Boston │ ├────────┼────────┼────────┤ │ Total │ 55 │ * │ └────────┴────────┴────────┘ ` if !visualCheck(t, "StreamBasic", buf.String(), expected) { fmt.Println(st.Debug()) t.Fail() } }) t.Run("TestStreamBasicGlobal", func(t *testing.T) { buf.Reset() st := createStreamTable(t, &buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}}, Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignRight}}}), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), ) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } st.Header([]string{"Name", "Age", "City"}) st.Append([]string{"Alice", "25", "New York"}) st.Append([]string{"Bob", "30", "Boston"}) st.Footer([]string{"Total", "55", "*"}) err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } // Widths: Name(5)+2=7, Age(3)+2=5, City(8)+2=10 expected := ` ┌────────┬────────┬────────┐ │ NAME │ AGE │ CITY │ ├────────┼────────┼────────┤ │ Alice │ 25 │ New │ │ │ │ York │ │ Bob │ 30 │ Boston │ ├────────┼────────┼────────┤ │ Total │ 55 │ * │ └────────┴────────┴────────┘ ` if !visualCheck(t, "StreamBasic", buf.String(), expected) { fmt.Println(st.Debug()) t.Fail() } }) } // TestStreamWithFooterAlign tests streaming table with footer and custom alignments. func TestStreamWithFooterAlign(t *testing.T) { var buf bytes.Buffer st := createStreamTable(t, &buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, Row: tw.CellConfig{ Alignment: tw.CellAlignment{ Global: tw.AlignLeft, PerColumn: []tw.Align{tw.AlignLeft, tw.AlignCenter, tw.AlignRight}, }, }, Footer: tw.CellConfig{ Alignment: tw.CellAlignment{ Global: tw.AlignRight, PerColumn: []tw.Align{tw.AlignLeft, tw.AlignCenter, tw.AlignRight}, }, }, }), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true})) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } st.Header([]string{"Item", "Qty", "Price"}) // Widths: 4+2=6, 3+2=5, 5+2=7 st.Append([]string{"Item 1", "2", "1000.00"}) // Needs: 6+2=8, 1+2=3, 7+2=9 st.Append([]string{"Item 2", "10", "25.50"}) // Needs: 6+2=8, 2+2=4, 5+2=7 st.Footer([]string{"", "Total", "1025.50"}) // Needs: 0+2=2, 5+2=7, 7+2=9 err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } // Max widths: [8, 7, 9] expected := ` ┌────────┬────────┬─────────┐ │ ITEM │ QTY │ PRICE │ ├────────┼────────┼─────────┤ │ Item 1 │ 2 │ 1000.00 │ │ Item 2 │ 10 │ 25.50 │ ├────────┼────────┼─────────┤ │ │ Total │ 1025.50 │ └────────┴────────┴─────────┘ ` if !visualCheck(t, "StreamWithFooterAlign", buf.String(), expected) { fmt.Println("--- DEBUG LOG ---") fmt.Println(st.Debug().String()) t.Fail() } } // TestStreamNoHeaderASCII tests streaming table without header using ASCII symbols. func TestStreamNoHeaderASCII(t *testing.T) { var buf bytes.Buffer st := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}}}), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: tw.NewSymbols(tw.StyleASCII)})), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), ) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } st.Append([]string{"Regular", "line", "1"}) // Widths: 7+2=9, 4+2=6, 1+2=3 st.Append([]string{"Thick", "thick", "2"}) err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } expected := ` +-----------+--------+--------+ | Regular | line | 1 | | Thick | thick | 2 | +-----------+--------+--------+ ` if !visualCheck(t, "StreamNoHeaderASCII", buf.String(), expected) { fmt.Println(st.Debug().String()) t.Fail() } } func TestBorders(t *testing.T) { data := [][]string{{"A", "B"}, {"C", "D"}} //widths := map[int]int{0: 3, 1: 3} // Content (1) + padding (1+1) = 3 tests := []struct { name string borders tw.Border expected string }{ { name: "All Off", borders: tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off}, expected: ` A │ B C │ D `, }, { name: "No Left/Right", borders: tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.On, Bottom: tw.On}, expected: ` ───┬─── A │ B C │ D ───┴─── `, }, { name: "No Top/Bottom", borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.Off, Bottom: tw.Off}, expected: ` │ A │ B │ │ C │ D │ `, }, { name: "Only Left", borders: tw.Border{Left: tw.On, Right: tw.Off, Top: tw.Off, Bottom: tw.Off}, expected: ` │ A │ B │ C │ D `, }, { name: "Default", borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On}, expected: ` ┌───┬───┐ │ A │ B │ │ C │ D │ └───┴───┘ `, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var buf bytes.Buffer r := renderer.NewBlueprint( tw.Rendition{ Borders: tt.borders, }, ) st := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}}}), tablewriter.WithRenderer(r), tablewriter.WithDebug(false), ) st.Append(data[0]) st.Append(data[1]) err := st.Render() if err != nil { t.Fatalf("End failed: %v", err) } if !visualCheck(t, "StreamBorders_"+tt.name, buf.String(), tt.expected) { fmt.Printf("--- DEBUG LOG for %s ---\n", tt.name) fmt.Println(st.Debug().String()) t.Fail() } }) } } // TestStreamTruncation tests streaming table with long content truncation. func TestStreamTruncation(t *testing.T) { var buf bytes.Buffer st := createStreamTable(t, &buf, tablewriter.WithConfig( tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, Row: tw.CellConfig{ Alignment: tw.CellAlignment{ Global: tw.AlignLeft, }, Formatting: tw.CellFormatting{AutoWrap: tw.WrapTruncate}, ColMaxWidths: tw.CellWidth{Global: 13}, }, Stream: tw.StreamConfig{ Enable: true, }, Widths: tw.CellWidth{ PerColumn: map[int]int{0: 4, 1: 15, 2: 8}, }, })) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } st.Header([]string{"ID", "Description", "Status"}) // Fits: 2<=4, 11<=15, 6<=8 st.Append([]string{"1", "This description is quite long", "OK"}) // Truncates: 1<=4, 30>15 -> "This descript…", 2<=8 st.Append([]string{"2", "Short desc", "DONE"}) // Fits: 1<=4, 10<=15, 4<=8 st.Append([]string{"3", "Another long one needing truncation", "ERR"}) // Truncates: 1<=4, 35>15 -> "Another long …", 3<=8 err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } // Widths: [4, 15, 8] expected := ` ┌────┬───────────────┬────────┐ │ ID │ DESCRIPTION │ STATUS │ ├────┼───────────────┼────────┤ │ 1 │ This descri… │ OK │ │ 2 │ Short desc │ DONE │ │ 3 │ Another lon… │ ERR │ └────┴───────────────┴────────┘ ` if !visualCheck(t, "StreamTruncation", buf.String(), expected) { fmt.Println("--- DEBUG LOG ---") fmt.Println(st.Debug().String()) t.Fail() } } // TestStreamCustomPadding tests streaming table with custom padding. func TestStreamCustomPadding(t *testing.T) { var buf bytes.Buffer st := createStreamTable(t, &buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{ Padding: tw.CellPadding{Global: tw.Padding{Left: ">>", Right: "<<"}}, }, Row: tw.CellConfig{ Padding: tw.CellPadding{Global: tw.Padding{Left: ">>", Right: "<<"}}, }, Stream: tw.StreamConfig{ Enable: true, }, Widths: tw.CellWidth{ PerColumn: map[int]int{0: 7, 1: 7}, }, }), tablewriter.WithDebug(false)) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } st.Header([]string{"Head1", "Head2"}) // Truncates: 5>3 -> "He…" st.Append([]string{"R1C1", "R1C2"}) // Truncates: 4>3 -> "R1…" err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } expected := ` ┌───────┬───────┐ │>>H…<<<│>>H…<<<│ ├───────┼───────┤ │>>R1C<<│>>R1C<<│ └───────┴───────┘ ` if !visualCheck(t, "StreamCustomPadding", buf.String(), expected) { fmt.Println("--- DEBUG LOG ---") fmt.Println(st.Debug().String()) t.Fail() } } // TestStreamEmptyCells tests streaming table with empty and sparse cells. func TestStreamEmptyCells(t *testing.T) { var buf bytes.Buffer st := createStreamTable(t, &buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}}, Stream: tw.StreamConfig{ Enable: true, }, Widths: tw.CellWidth{ Global: 20, }}), tablewriter.WithDebug(false)) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } st.Header([]string{"H1", "", "H3"}) // Widths: 2+2=4, 0+2=2->3, 2+2=4 st.Append([]string{"", "R1C2", ""}) // Needs: 0+2=2, 4+2=6, 0+2=2 st.Append([]string{"R2C1", "", "R2C3"}) // Needs: 4+2=6, 0+2=2, 4+2=6 st.Append([]string{"", "", ""}) // Needs: 0+2=2, 0+2=2, 0+2=2 err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } // Max widths: [6, 6, 6] expected := ` ┌──────┬──────┬──────┐ │ H 1 │ │ H 3 │ ├──────┼──────┼──────┤ │ │ R1C2 │ │ │ R2C1 │ │ R2C3 │ │ │ │ │ └──────┴──────┴──────┘ ` if !visualCheck(t, "StreamEmptyCells", buf.String(), expected) { fmt.Println("--- DEBUG LOG ---") fmt.Println(st.Debug().String()) t.Fail() } } // TestStreamOnlyHeader tests streaming table with only a header. func TestStreamOnlyHeader(t *testing.T) { var buf bytes.Buffer st := createStreamTable(t, &buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, }), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{Enable: true})) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } st.Header([]string{"Header1", "Header2"}) // Widths: 7+2=9, 7+2=9 err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } expected := ` ┌───────────┬───────────┐ │ HEADER 1 │ HEADER 2 │ └───────────┴───────────┘ ` if !visualCheck(t, "StreamOnlyHeader", buf.String(), expected) { fmt.Println("--- DEBUG LOG ---") fmt.Println(st.Debug().String()) t.Fail() } } // TestStreamOnlyHeaderNoHeaderLine tests streaming table with only a header and no header line. func TestStreamOnlyHeaderNoHeaderLine(t *testing.T) { var buf bytes.Buffer st := createStreamTable(t, &buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, }), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), ) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } st.Header([]string{"Header1", "Header2"}) // Fits: 7<=9, 7<=9 err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } expected := ` ┌───────────┬───────────┐ │ HEADER 1 │ HEADER 2 │ └───────────┴───────────┘ ` if !visualCheck(t, "StreamOnlyHeaderNoHeaderLine", buf.String(), expected) { fmt.Println("--- DEBUG LOG ---") fmt.Println(st.Debug().String()) t.Fail() } } func TestStreamSlowOutput(t *testing.T) { var buf bytes.Buffer st := createStreamTable(t, &buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}}, }), tablewriter.WithStreaming(tw.StreamConfig{Enable: true}), ) //Test Start() err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } buf.Reset() //Test Header() time.Sleep(100 * time.Millisecond) st.Header([]string{"Event", "Timestamp"}) lastLine := getLastContentLine(&buf) if !strings.Contains(lastLine, "EVENT") || !strings.Contains(lastLine, "TIMESTAMP") { t.Errorf("Header missing expected columns:\nGot: %q", lastLine) } buf.Reset() //Test Rows for i := 1; i <= 3; i++ { time.Sleep(100 * time.Millisecond) err = st.Append([]string{fmt.Sprintf("Row %d", i), time.Now().Format("15:04:05.000")}) if err != nil { t.Fatalf("Row %d failed: %v", i, err) } lastLine = getLastContentLine(&buf) if !strings.Contains(lastLine, fmt.Sprintf("Row %d", i)) { t.Errorf("Row %d content missing:\nGot: %q", i, lastLine) } buf.Reset() } err = st.Close() if err != nil { t.Fatalf("Close failed: %v", err) } } func TestStreamFormating(t *testing.T) { var buf bytes.Buffer st := createStreamTable(t, &buf, tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}}, Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}}, Widths: tw.CellWidth{PerColumn: map[int]int{0: 12, 1: 8, 2: 10}}}), tablewriter.WithDebug(false), tablewriter.WithStreaming(tw.StreamConfig{ Enable: true, }), ) err := st.Start() if err != nil { t.Fatalf("Start failed: %v", err) } data := [][]any{ {Name{"Al i CE", " Ma SK"}, Age(25), "New York"}, {Name{"bOb", "mar le y"}, Age(30), "Boston"}, } st.Header([]string{"Name", "Age", "City"}) st.Bulk(data) err = st.Close() if err != nil { t.Fatalf("End failed: %v", err) } // Widths: Name(5)+2=7, Age(3)+2=5, City(8)+2=10 expected := ` ┌────────────┬────────┬──────────┐ │ NAME │ AGE │ CITY │ ├────────────┼────────┼──────────┤ │ Alice Mask │ 25yrs │ New York │ │ Bob Marley │ 30yrs │ Boston │ └────────────┴────────┴──────────┘ ` if !visualCheck(t, "StreamBasic", buf.String(), expected) { fmt.Println(st.Debug()) t.Fail() } } tablewriter-1.0.9/tests/struct_test.go000066400000000000000000000233151504170215200201220ustar00rootroot00000000000000package tests import ( "bytes" "fmt" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "testing" ) // Employee represents a struct for employee data, simulating a database record. type Employee struct { ID int Name string Age int Department string Salary float64 } // dummyDatabase simulates a database with employee records. type dummyDatabase struct { records []Employee } // fetchEmployees simulates fetching data from a database. func (db *dummyDatabase) fetchEmployees() []Employee { return db.records } // employeeStringer converts an Employee struct to a slice of strings for table rendering. func employeeStringer(e interface{}) []string { emp, ok := e.(Employee) if !ok { return []string{"Error: Invalid type"} } return []string{ fmt.Sprintf("%d", emp.ID), emp.Name, fmt.Sprintf("%d", emp.Age), emp.Department, fmt.Sprintf("%.2f", emp.Salary), } } // TestStructTableWithDB tests rendering a table from struct data fetched from a dummy database. func TestStructTableWithDB(t *testing.T) { // Initialize dummy database with sample data db := &dummyDatabase{ records: []Employee{ {ID: 1, Name: "Alice Smith", Age: 28, Department: "Engineering", Salary: 75000.50}, {ID: 2, Name: "Bob Johnson", Age: 34, Department: "Marketing", Salary: 62000.00}, {ID: 3, Name: "Charlie Brown", Age: 45, Department: "HR", Salary: 80000.75}, }, } // Configure table with custom settings config := tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{ AutoFormat: tw.On, }, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }, Row: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignLeft}, }, Footer: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignRight}, }, } // Create table with buffer and custom renderer var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(config), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleRounded), // Use rounded Unicode style Settings: tw.Settings{ Separators: tw.Separators{ BetweenColumns: tw.On, BetweenRows: tw.Off, }, Lines: tw.Lines{ ShowHeaderLine: tw.On, }, }, })), tablewriter.WithStringer(employeeStringer), ) // Set the stringer for converting Employee structs // Set header table.Header([]string{"ID", "Name", "Age", "Department", "Salary"}) // Fetch data from "database" and append to table employees := db.fetchEmployees() for _, emp := range employees { if err := table.Append(emp); err != nil { t.Fatalf("Failed to append employee: %v", err) } } // Add a footer with a total salary totalSalary := 0.0 for _, emp := range employees { totalSalary += emp.Salary } table.Footer([]string{"", "", "", "Total", fmt.Sprintf("%.2f", totalSalary)}) // Render the table if err := table.Render(); err != nil { t.Fatalf("Failed to render table: %v", err) } // Expected output expected := ` ╭────┬───────────────┬─────┬─────────────┬───────────╮ │ ID │ NAME │ AGE │ DEPARTMENT │ SALARY │ ├────┼───────────────┼─────┼─────────────┼───────────┤ │ 1 │ Alice Smith │ 28 │ Engineering │ 75000.50 │ │ 2 │ Bob Johnson │ 34 │ Marketing │ 62000.00 │ │ 3 │ Charlie Brown │ 45 │ HR │ 80000.75 │ ├────┼───────────────┼─────┼─────────────┼───────────┤ │ │ │ │ Total │ 217001.25 │ ╰────┴───────────────┴─────┴─────────────┴───────────╯ ` // Visual check if !visualCheck(t, "StructTableWithDB", buf.String(), expected) { t.Log(table.Debug()) } } func TestAutoHeaderScenarios(t *testing.T) { type Basic struct { Foo int Bar string baz bool // unexported } type WithTags struct { ID int `json:"id"` Name string `json:"name,omitempty"` Age int `json:"-"` City string `json:"location"` } type Omitted struct { SkipMe string `json:"-"` KeepMe string } type NoTags struct { Field1 string field2 int // unexported } type Embedded struct { WithTags Extra string } type PointerTest struct { Value string } tests := []struct { name string data interface{} enable bool preHeaders []string expected string }{ { name: "BasicStruct", data: []Basic{{1, "test", true}, {2, "test2", false}}, enable: true, expected: ` ┌─────┬───────┐ │ FOO │ BAR │ ├─────┼───────┤ │ 1 │ test │ │ 2 │ test2 │ └─────┴───────┘ `, }, { name: "WithTags", data: []WithTags{{1, "John", 30, "NY"}, {2, "", 0, "LA"}}, enable: true, expected: ` ┌────┬──────┬──────────┐ │ ID │ NAME │ LOCATION │ ├────┼──────┼──────────┤ │ 1 │ John │ NY │ │ 2 │ │ LA │ └────┴──────┴──────────┘ `, }, { name: "OmittedFields", data: []Omitted{{"skip", "keep"}, {"skip2", "keep2"}}, enable: true, expected: ` ┌────────┐ │ KEEPME │ ├────────┤ │ keep │ │ keep2 │ └────────┘ `, }, { name: "NoTags", data: []NoTags{{"val1", 42}, {"val2", 43}}, enable: true, expected: ` ┌─────────┐ │ FIELD 1 │ ├─────────┤ │ val1 │ │ val2 │ └─────────┘ `, }, { name: "Embedded", data: []Embedded{{WithTags{1, "John", 30, "NY"}, "Extra"}, {WithTags{2, "Doe", 25, "LA"}, "Value"}}, enable: true, expected: ` ┌────┬──────┬──────────┬───────┐ │ ID │ NAME │ LOCATION │ EXTRA │ ├────┼──────┼──────────┼───────┤ │ 1 │ John │ NY │ Extra │ │ 2 │ Doe │ LA │ Value │ └────┴──────┴──────────┴───────┘ `, }, { name: "PointerToStruct", data: []*PointerTest{{"Value1"}, {"Value2"}}, enable: true, expected: ` ┌────────┐ │ VALUE │ ├────────┤ │ Value1 │ │ Value2 │ └────────┘ `, }, { name: "SliceOfPointers", data: []*WithTags{{1, "John", 30, "NY"}, {2, "Doe", 25, "LA"}}, enable: true, expected: ` ┌────┬──────┬──────────┐ │ ID │ NAME │ LOCATION │ ├────┼──────┼──────────┤ │ 1 │ John │ NY │ │ 2 │ Doe │ LA │ └────┴──────┴──────────┘ `, }, { name: "NonStruct", data: [][]string{{"A", "B"}, {"C", "D"}}, enable: false, expected: ` ┌───┬───┐ │ A │ B │ │ C │ D │ └───┴───┘ `, }, { name: "EmptySlice", data: []WithTags{}, expected: ``, }, { name: "enabled", data: []WithTags{{1, "John", 30, "NY"}}, enable: true, expected: ` ┌────┬──────┬──────────┐ │ ID │ NAME │ LOCATION │ ├────┼──────┼──────────┤ │ 1 │ John │ NY │ └────┴──────┴──────────┘ `, // No header, falls back to string reps }, { name: "Disabled", data: []WithTags{{1, "John", 30, "NY"}}, enable: false, expected: ` ┌───┬──────┬────┐ │ 1 │ John │ NY │ └───┴──────┴────┘ `, // No header, falls back to string reps }, { name: "PreExistingHeaders", data: []WithTags{{1, "John", 30, "NY"}}, preHeaders: []string{"CustomID", "CustomName", "CustomCity"}, enable: true, expected: ` ┌───────────┬─────────────┬─────────────┐ │ CUSTOM ID │ CUSTOM NAME │ CUSTOM CITY │ ├───────────┼─────────────┼─────────────┤ │ 1 │ John │ NY │ └───────────┴─────────────┴─────────────┘ `, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf) if tt.enable { table.Configure(func(cfg *tablewriter.Config) { cfg.Behavior.Structs.AutoHeader = tw.On }) } if len(tt.preHeaders) > 0 { table.Header(tt.preHeaders) } err := table.Bulk(tt.data) if err != nil { t.Fatalf("Bulk failed: %v", err) } table.Render() visualCheck(t, tt.name, buf.String(), tt.expected) }) } } tablewriter-1.0.9/tests/svg_test.go000066400000000000000000000573041504170215200174020ustar00rootroot00000000000000package tests import ( "bytes" "encoding/xml" "math" "regexp" "strconv" "strings" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) // defaultSVGConfigForTests provides a consistent SVG config for tests. // Parameter debug enables debug logging if true. // Returns an SVGConfig struct with default settings. func defaultSVGConfigForTests(debug bool) renderer.SVGConfig { return renderer.SVGConfig{ FontFamily: "Arial", FontSize: 10, LineHeightFactor: 1.2, Padding: 3, StrokeWidth: 1, StrokeColor: "black", HeaderBG: "#DDD", RowBG: "#FFF", RowAltBG: "#EEE", FooterBG: "#DDD", HeaderColor: "#000", RowColor: "#000", FooterColor: "#000", ApproxCharWidthFactor: 0.6, MinColWidth: 20, RenderTWConfigOverrides: true, Debug: debug, } } // xmlNode represents a node in the XML tree for structural comparison type xmlNode struct { XMLName xml.Name Attrs []xml.Attr `xml:",any,attr"` Content string `xml:",chardata"` Nodes []xmlNode `xml:",any"` } // normalizeSVG performs lightweight normalization for fast comparison func normalizeSVG(s string) string { s = strings.TrimSpace(s) // Remove comments s = regexp.MustCompile(``).ReplaceAllString(s, "") // Normalize whitespace s = regexp.MustCompile(`\s+`).ReplaceAllString(s, " ") s = regexp.MustCompile(`>\s+<`).ReplaceAllString(s, "><") // Normalize self-closing tags s = regexp.MustCompile(`\s*/>`).ReplaceAllString(s, "/>") // Normalize numeric values (1.0 -> 1, 0.500 -> 0.5) s = regexp.MustCompile(`(\d+)\.0([^0-9])`).ReplaceAllString(s, "$1$2") s = regexp.MustCompile(`(\d+\.\d+)0([^0-9])`).ReplaceAllString(s, "$1$2") s = regexp.MustCompile(`\.0([^0-9])`).ReplaceAllString(s, "$1") return s } // visualCheckSVG is the main comparison function func visualCheckSVG(t *testing.T, testName, actual, expected string) bool { t.Helper() // First try normalized string comparison normActual := normalizeSVG(actual) normExpected := normalizeSVG(expected) if normActual == normExpected { return true } // If strings differ, try structural comparison ok, err := compareSVGStructure(normActual, normExpected) if err != nil { t.Logf("Structural comparison failed: %v", err) } else if ok { return true } // Both comparisons failed - show formatted diff t.Errorf("%s: SVG output mismatch", testName) showFormattedDiff(t, normActual, normExpected) return false } // compareSVGStructure does structural XML comparison func compareSVGStructure(actual, expected string) (bool, error) { var actualNode, expectedNode xmlNode if err := xml.Unmarshal([]byte(actual), &actualNode); err != nil { return false, err } if err := xml.Unmarshal([]byte(expected), &expectedNode); err != nil { return false, err } return compareXMLNodes(actualNode, expectedNode), nil } // compareXMLNodes recursively compares XML nodes func compareXMLNodes(a, b xmlNode) bool { if a.XMLName != b.XMLName { return false } // Compare attributes if len(a.Attrs) != len(b.Attrs) { return false } attrMap := make(map[xml.Name]string) for _, attr := range a.Attrs { attrMap[attr.Name] = attr.Value } for _, attr := range b.Attrs { aVal, ok := attrMap[attr.Name] if !ok { return false } if isNumericAttribute(attr.Name.Local) { if !compareFloatStrings(aVal, attr.Value) { return false } } else if aVal != attr.Value { return false } } // Compare content if strings.TrimSpace(a.Content) != strings.TrimSpace(b.Content) { return false } // Compare children if len(a.Nodes) != len(b.Nodes) { return false } for i := range a.Nodes { if !compareXMLNodes(a.Nodes[i], b.Nodes[i]) { return false } } return true } // Helper functions func isNumericAttribute(name string) bool { numericAttrs := map[string]bool{ "x": true, "y": true, "width": true, "height": true, "x1": true, "y1": true, "x2": true, "y2": true, "font-size": true, "stroke-width": true, } return numericAttrs[name] } func compareFloatStrings(a, b string) bool { f1, err1 := strconv.ParseFloat(a, 64) f2, err2 := strconv.ParseFloat(b, 64) if err1 != nil || err2 != nil { return a == b } const epsilon = 0.0001 return math.Abs(f1-f2) < epsilon } func showFormattedDiff(t *testing.T, actual, expected string) { format := func(s string) string { var buf bytes.Buffer enc := xml.NewEncoder(&buf) enc.Indent("", " ") if err := enc.Encode(xml.NewDecoder(strings.NewReader(s))); err != nil { return s } return buf.String() } t.Logf("--- EXPECTED (Formatted) ---\n%s", format(expected)) t.Logf("--- ACTUAL (Formatted) ---\n%s", format(actual)) } // TestSVGBasicTable tests rendering a basic SVG table. // Parameter t is the testing context. // No return value; logs debug info on failure. func TestSVGBasicTable(t *testing.T) { var buf bytes.Buffer svgCfg := defaultSVGConfigForTests(true) table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewSVG(svgCfg))) table.Header([]string{"Name", "Age", "City"}) table.Append([]string{"Alice", "25", "New York"}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() expected := `NAMEAGECITYAlice25New YorkBob30Boston` if !visualCheckSVG(t, "SVGBasicTable", buf.String(), expected) { t.Log("--- Debug Log for SVGBasicTable ---") t.Log(table.Debug()) } } // TestSVGEmptyTable tests rendering empty and header-only SVG tables. // Parameter t is the testing context. // No return value; logs debug info on failure. func TestSVGEmptyTable(t *testing.T) { var buf bytes.Buffer svgCfg := defaultSVGConfigForTests(false) table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewSVG(svgCfg))) table.Render() expectedEmpty := `` if !visualCheckSVG(t, "SVGEmptyTable_CompletelyEmpty", buf.String(), expectedEmpty) { t.Logf("Empty table output: '%s'", buf.String()) t.Log(table.Debug()) } buf.Reset() tableHeaderOnly := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewSVG(svgCfg))) tableHeaderOnly.Header([]string{"Test"}) tableHeaderOnly.Render() expectedHeaderOnly := `TEST` if !visualCheckSVG(t, "SVGEmptyTable_HeaderOnly", buf.String(), expectedHeaderOnly) { t.Log(table.Debug()) } } // TestSVGHierarchicalMerge tests SVG rendering with hierarchical merging. // Parameter t is the testing context. // No return value; logs debug info on failure. func TestSVGHierarchicalMerge(t *testing.T) { var buf bytes.Buffer svgCfg := defaultSVGConfigForTests(false) table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewSVG(svgCfg)), tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}}, Row: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical}}, }), ) table.Header([]string{"L1", "L2", "L3"}) table.Append([]string{"A", "a", "1"}) table.Append([]string{"A", "b", "2"}) table.Append([]string{"A", "b", "3"}) table.Render() expected := `L 1L 2L 3Aa1b23` if !visualCheckSVG(t, "SVGHierarchicalMerge", buf.String(), expected) { t.Log(table.Debug()) } } // TestSVGMultiLineContent tests SVG rendering with multi-line content. // Parameter t is the testing context. // No return value; logs debug info on failure. func TestSVGMultiLineContent(t *testing.T) { var buf bytes.Buffer svgCfg := defaultSVGConfigForTests(false) table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewSVG(svgCfg))) table.Header([]string{"Description", "Status"}) table.Append([]string{"Line 1\nLine 2", "OK"}) table.Render() expected := `DESCRIPTIONSTATUSLine 1OKLine 2` if !visualCheckSVG(t, "SVGMultiLineContent", buf.String(), expected) { t.Log(table.Debug()) } } // TestSVGPaddingAndFont tests SVG rendering with custom padding and font. // Parameter t is the testing context. // No return value; logs debug info on failure. func TestSVGPaddingAndFont(t *testing.T) { var buf bytes.Buffer svgCfg := defaultSVGConfigForTests(false) svgCfg.Padding = 10 svgCfg.FontSize = 16 svgCfg.FontFamily = "Verdana" table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewSVG(svgCfg))) table.Header([]string{"Test"}) table.Render() expected := `TEST` if !visualCheckSVG(t, "SVGPaddingAndFont", buf.String(), expected) { t.Log(table.Debug()) } } // TestSVGVerticalMerge tests SVG rendering with vertical merging. // Parameter t is the testing context. // No return value; logs debug info on failure. func TestSVGVerticalMerge(t *testing.T) { var buf bytes.Buffer svgCfg := defaultSVGConfigForTests(false) table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewSVG(svgCfg)), tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeVertical}}, }), ) table.Header([]string{"Cat", "Item"}) table.Append([]string{"Fruit", "Apple"}) table.Append([]string{"Fruit", "Banana"}) table.Render() expected := `CATITEMFruitAppleBanana` if !visualCheckSVG(t, "SVGVerticalMerge", buf.String(), expected) { t.Log(table.Debug()) } } // TestSVGWithFooterAndAlignment tests SVG with footer and alignments. // Parameter t is the testing context. // No return value; skips test as experimental. func TestSVGWithFooterAndAlignment(t *testing.T) { var buf bytes.Buffer svgCfg := defaultSVGConfigForTests(false) table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewSVG(svgCfg)), tablewriter.WithHeaderConfig(tw.CellConfig{ Formatting: tw.CellFormatting{AutoFormat: tw.On}, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }), tablewriter.WithRowConfig(tw.CellConfig{ Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignLeft, tw.AlignRight, tw.AlignCenter}}, }), tablewriter.WithFooterConfig(tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignRight}, }), ) table.Header([]string{"Item", "Qty", "Price"}) table.Append([]string{"Apple", "5", "1.20"}) table.Append([]string{"Banana", "12", "0.35"}) table.Footer([]string{"", "Total", "7.20"}) table.Render() expected := `ITEMQTYPRICEApple51.20Banana120.35Total7.20` if !visualCheckSVG(t, "SVGWithFooterAndAlignment", buf.String(), expected) { t.Log("--- Debug Log for SVGWithFooterAndAlignment ---") t.Log(table.Debug()) } } // TestSVGColumnAlignmentOverride tests SVG with column alignment overrides. // Parameter t is the testing context. // No return value; func TestSVGColumnAlignmentOverride(t *testing.T) { var buf bytes.Buffer svgCfg := defaultSVGConfigForTests(false) table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewSVG(svgCfg)), tablewriter.WithHeaderConfig(tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}, Formatting: tw.CellFormatting{AutoFormat: tw.Off}}), tablewriter.WithRowConfig(tw.CellConfig{ Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.AlignRight, tw.AlignCenter, tw.AlignLeft}}, }), ) table.Header([]string{"H1", "H2", "H3"}) table.Append([]string{"R1C1", "R1C2", "R1C3"}) table.Render() expected := `H1H2H3R1C1R1C2R1C3` if !visualCheckSVG(t, "SVGColumnAlignmentOverride", buf.String(), expected) { t.Log(table.Debug()) } } // TestSVGHorizontalMerge tests SVG with horizontal merging. // Parameter t is the testing context. // No return value; func TestSVGHorizontalMerge(t *testing.T) { var buf bytes.Buffer svgCfg := defaultSVGConfigForTests(false) table := tablewriter.NewTable(&buf, tablewriter.WithRenderer(renderer.NewSVG(svgCfg)), tablewriter.WithConfig(tablewriter.Config{ Header: tw.CellConfig{ Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }, Row: tw.CellConfig{Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}, Alignment: tw.CellAlignment{Global: tw.AlignLeft}}, }), ) table.Header([]string{"A", "Merged Header", "Merged Header"}) table.Append([]string{"Data 1", "Data 2", "Data 2"}) table.Render() expected := `AMERGED HEADERData 1Data 2` if !visualCheckSVG(t, "SVGHorizontalMerge", buf.String(), expected) { t.Log("--- Debug Log for SVGHorizontalMerge ---") t.Log(table.Debug()) } } tablewriter-1.0.9/tests/table_bench_test.go000066400000000000000000000051101504170215200210150ustar00rootroot00000000000000package tests import ( "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "io" "strings" "testing" ) type Country string func (c Country) String() string { return strings.ToUpper(string(c)) } func BenchmarkBlueprint(b *testing.B) { table := tablewriter.NewTable(io.Discard, tablewriter.WithRenderer(renderer.NewBlueprint())) table.Header([]string{"Name", "Age", "City"}) for i := 0; i < b.N; i++ { table.Append([]any{"Alice", Age(25), Country("New York")}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() } } func BenchmarkOcean(b *testing.B) { table := tablewriter.NewTable(io.Discard, tablewriter.WithRenderer(renderer.NewOcean())) table.Header([]string{"Name", "Age", "City"}) for i := 0; i < b.N; i++ { table.Append([]any{"Alice", Age(25), Country("New York")}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() } } func BenchmarkMarkdown(b *testing.B) { table := tablewriter.NewTable(io.Discard, tablewriter.WithRenderer(renderer.NewMarkdown())) table.Header([]string{"Name", "Age", "City"}) for i := 0; i < b.N; i++ { table.Append([]any{"Alice", Age(25), Country("New York")}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() } } func BenchmarkColorized(b *testing.B) { table := tablewriter.NewTable(io.Discard, tablewriter.WithRenderer(renderer.NewColorized())) table.Header([]string{"Name", "Age", "City"}) for i := 0; i < b.N; i++ { table.Append([]any{"Alice", Age(25), Country("New York")}) table.Append([]string{"Bob", "30", "Boston"}) table.Render() } } func BenchmarkStreamBlueprint(b *testing.B) { table := tablewriter.NewTable(io.Discard, tablewriter.WithRenderer(renderer.NewBlueprint()), tablewriter.WithStreaming(tw.StreamConfig{Enable: true})) err := table.Start() if err != nil { b.Fatal(err) } table.Header([]string{"Name", "Age", "City"}) for i := 0; i < b.N; i++ { table.Append([]any{"Alice", Age(25), Country("New York")}) table.Append([]string{"Bob", "30", "Boston"}) } err = table.Close() if err != nil { b.Fatal(err) } } func BenchmarkStreamOcean(b *testing.B) { table := tablewriter.NewTable(io.Discard, tablewriter.WithRenderer(renderer.NewOcean()), tablewriter.WithStreaming(tw.StreamConfig{Enable: true})) err := table.Start() if err != nil { b.Fatal(err) } table.Header([]string{"Name", "Age", "City"}) for i := 0; i < b.N; i++ { table.Append([]any{"Alice", Age(25), Country("New York")}) table.Append([]string{"Bob", "30", "Boston"}) } err = table.Close() if err != nil { b.Fatal(err) } } tablewriter-1.0.9/tw/000077500000000000000000000000001504170215200144745ustar00rootroot00000000000000tablewriter-1.0.9/tw/cell.go000066400000000000000000000044271504170215200157510ustar00rootroot00000000000000package tw // CellFormatting holds formatting options for table cells. type CellFormatting struct { AutoWrap int // Wrapping behavior (e.g., WrapTruncate, WrapNormal) MergeMode int // Bitmask for merge behavior (e.g., MergeHorizontal, MergeVertical) // Changed form bool to State // See https://github.com/olekukonko/tablewriter/issues/261 AutoFormat State // Enables automatic formatting (e.g., title case for headers) // Deprecated: kept for compatibility // will be removed soon Alignment Align // Text alignment within the cell (e.g., Left, Right, Center) } // CellPadding defines padding settings for table cells. type CellPadding struct { Global Padding // Default padding applied to all cells PerColumn []Padding // Column-specific padding overrides } // CellFilter defines filtering functions for cell content. type CellFilter struct { Global func([]string) []string // Processes the entire row PerColumn []func(string) string // Processes individual cells by column } // CellCallbacks holds callback functions for cell processing. // Note: These are currently placeholders and not fully implemented. type CellCallbacks struct { Global func() // Global callback applied to all cells PerColumn []func() // Column-specific callbacks } // CellAlignment defines alignment settings for table cells. type CellAlignment struct { Global Align // Default alignment applied to all cells PerColumn []Align // Column-specific alignment overrides } // CellConfig combines formatting, padding, and callback settings for a table section. type CellConfig struct { Formatting CellFormatting // Cell formatting options Padding CellPadding // Padding configuration Callbacks CellCallbacks // Callback functions (unused) Filter CellFilter // Function to filter cell content (renamed from Filter Filter) Alignment CellAlignment // Alignment configuration for cells ColMaxWidths CellWidth // Per-column maximum width overrides // Deprecated: use Alignment.PerColumn instead. Will be removed in a future version. // will be removed soon ColumnAligns []Align // Per-column alignment overrides } type CellWidth struct { Global int PerColumn Mapper[int, int] } func (c CellWidth) Constrained() bool { return c.Global > 0 || c.PerColumn.Len() > 0 } tablewriter-1.0.9/tw/deprecated.go000066400000000000000000000166711504170215200171360ustar00rootroot00000000000000package tw // Deprecated: SymbolASCII is deprecated; use Glyphs with StyleASCII instead. // this will be removed soon type SymbolASCII struct{} // SymbolASCII symbol methods func (s *SymbolASCII) Name() string { return StyleNameASCII.String() } func (s *SymbolASCII) Center() string { return "+" } func (s *SymbolASCII) Row() string { return "-" } func (s *SymbolASCII) Column() string { return "|" } func (s *SymbolASCII) TopLeft() string { return "+" } func (s *SymbolASCII) TopMid() string { return "+" } func (s *SymbolASCII) TopRight() string { return "+" } func (s *SymbolASCII) MidLeft() string { return "+" } func (s *SymbolASCII) MidRight() string { return "+" } func (s *SymbolASCII) BottomLeft() string { return "+" } func (s *SymbolASCII) BottomMid() string { return "+" } func (s *SymbolASCII) BottomRight() string { return "+" } func (s *SymbolASCII) HeaderLeft() string { return "+" } func (s *SymbolASCII) HeaderMid() string { return "+" } func (s *SymbolASCII) HeaderRight() string { return "+" } // Deprecated: SymbolUnicode is deprecated; use Glyphs with appropriate styles (e.g., StyleLight, StyleHeavy) instead. // this will be removed soon type SymbolUnicode struct { row string column string center string corners [9]string // [topLeft, topMid, topRight, midLeft, center, midRight, bottomLeft, bottomMid, bottomRight] } // SymbolUnicode symbol methods func (s *SymbolUnicode) Name() string { return "unicode" } func (s *SymbolUnicode) Center() string { return s.center } func (s *SymbolUnicode) Row() string { return s.row } func (s *SymbolUnicode) Column() string { return s.column } func (s *SymbolUnicode) TopLeft() string { return s.corners[0] } func (s *SymbolUnicode) TopMid() string { return s.corners[1] } func (s *SymbolUnicode) TopRight() string { return s.corners[2] } func (s *SymbolUnicode) MidLeft() string { return s.corners[3] } func (s *SymbolUnicode) MidRight() string { return s.corners[5] } func (s *SymbolUnicode) BottomLeft() string { return s.corners[6] } func (s *SymbolUnicode) BottomMid() string { return s.corners[7] } func (s *SymbolUnicode) BottomRight() string { return s.corners[8] } func (s *SymbolUnicode) HeaderLeft() string { return s.MidLeft() } func (s *SymbolUnicode) HeaderMid() string { return s.Center() } func (s *SymbolUnicode) HeaderRight() string { return s.MidRight() } // Deprecated: SymbolMarkdown is deprecated; use Glyphs with StyleMarkdown instead. // this will be removed soon type SymbolMarkdown struct{} // SymbolMarkdown symbol methods func (s *SymbolMarkdown) Name() string { return StyleNameMarkdown.String() } func (s *SymbolMarkdown) Center() string { return "|" } func (s *SymbolMarkdown) Row() string { return "-" } func (s *SymbolMarkdown) Column() string { return "|" } func (s *SymbolMarkdown) TopLeft() string { return "" } func (s *SymbolMarkdown) TopMid() string { return "" } func (s *SymbolMarkdown) TopRight() string { return "" } func (s *SymbolMarkdown) MidLeft() string { return "|" } func (s *SymbolMarkdown) MidRight() string { return "|" } func (s *SymbolMarkdown) BottomLeft() string { return "" } func (s *SymbolMarkdown) BottomMid() string { return "" } func (s *SymbolMarkdown) BottomRight() string { return "" } func (s *SymbolMarkdown) HeaderLeft() string { return "|" } func (s *SymbolMarkdown) HeaderMid() string { return "|" } func (s *SymbolMarkdown) HeaderRight() string { return "|" } // Deprecated: SymbolNothing is deprecated; use Glyphs with StyleNone instead. // this will be removed soon type SymbolNothing struct{} // SymbolNothing symbol methods func (s *SymbolNothing) Name() string { return StyleNameNothing.String() } func (s *SymbolNothing) Center() string { return "" } func (s *SymbolNothing) Row() string { return "" } func (s *SymbolNothing) Column() string { return "" } func (s *SymbolNothing) TopLeft() string { return "" } func (s *SymbolNothing) TopMid() string { return "" } func (s *SymbolNothing) TopRight() string { return "" } func (s *SymbolNothing) MidLeft() string { return "" } func (s *SymbolNothing) MidRight() string { return "" } func (s *SymbolNothing) BottomLeft() string { return "" } func (s *SymbolNothing) BottomMid() string { return "" } func (s *SymbolNothing) BottomRight() string { return "" } func (s *SymbolNothing) HeaderLeft() string { return "" } func (s *SymbolNothing) HeaderMid() string { return "" } func (s *SymbolNothing) HeaderRight() string { return "" } // Deprecated: SymbolGraphical is deprecated; use Glyphs with StyleGraphical instead. // this will be removed soon type SymbolGraphical struct{} // SymbolGraphical symbol methods func (s *SymbolGraphical) Name() string { return StyleNameGraphical.String() } func (s *SymbolGraphical) Center() string { return "🟧" } // Orange square (matches mid junctions) func (s *SymbolGraphical) Row() string { return "🟥" } // Red square (matches corners) func (s *SymbolGraphical) Column() string { return "🟦" } // Blue square (vertical line) func (s *SymbolGraphical) TopLeft() string { return "🟥" } // Top-left corner func (s *SymbolGraphical) TopMid() string { return "🔳" } // Top junction func (s *SymbolGraphical) TopRight() string { return "🟥" } // Top-right corner func (s *SymbolGraphical) MidLeft() string { return "🟧" } // Left junction func (s *SymbolGraphical) MidRight() string { return "🟧" } // Right junction func (s *SymbolGraphical) BottomLeft() string { return "🟥" } // Bottom-left corner func (s *SymbolGraphical) BottomMid() string { return "🔳" } // Bottom junction func (s *SymbolGraphical) BottomRight() string { return "🟥" } // Bottom-right corner func (s *SymbolGraphical) HeaderLeft() string { return "🟧" } // Header left (matches mid junctions) func (s *SymbolGraphical) HeaderMid() string { return "🟧" } // Header middle (matches mid junctions) func (s *SymbolGraphical) HeaderRight() string { return "🟧" } // Header right (matches mid junctions) // Deprecated: SymbolMerger is deprecated; use Glyphs with StyleMerger instead. // this will be removed soon type SymbolMerger struct { row string column string center string corners [9]string // [TL, TM, TR, ML, CenterIdx(unused), MR, BL, BM, BR] } // SymbolMerger symbol methods func (s *SymbolMerger) Name() string { return StyleNameMerger.String() } func (s *SymbolMerger) Center() string { return s.center } // Main crossing symbol func (s *SymbolMerger) Row() string { return s.row } func (s *SymbolMerger) Column() string { return s.column } func (s *SymbolMerger) TopLeft() string { return s.corners[0] } func (s *SymbolMerger) TopMid() string { return s.corners[1] } // LevelHeader junction func (s *SymbolMerger) TopRight() string { return s.corners[2] } func (s *SymbolMerger) MidLeft() string { return s.corners[3] } // Left junction func (s *SymbolMerger) MidRight() string { return s.corners[5] } // Right junction func (s *SymbolMerger) BottomLeft() string { return s.corners[6] } func (s *SymbolMerger) BottomMid() string { return s.corners[7] } // LevelFooter junction func (s *SymbolMerger) BottomRight() string { return s.corners[8] } func (s *SymbolMerger) HeaderLeft() string { return s.MidLeft() } func (s *SymbolMerger) HeaderMid() string { return s.Center() } func (s *SymbolMerger) HeaderRight() string { return s.MidRight() } tablewriter-1.0.9/tw/fn.go000066400000000000000000000154031504170215200154310ustar00rootroot00000000000000// Package tw provides utility functions for text formatting, width calculation, and string manipulation // specifically tailored for table rendering, including handling ANSI escape codes and Unicode text. package tw import ( "github.com/olekukonko/tablewriter/pkg/twwidth" "math" // For mathematical operations like ceiling "strconv" // For string-to-number conversions "strings" // For string manipulation utilities "unicode" // For Unicode character classification "unicode/utf8" // For UTF-8 rune handling ) // Title normalizes and uppercases a label string for use in headers. // It replaces underscores and certain dots with spaces and trims whitespace. func Title(name string) string { origLen := len(name) rs := []rune(name) for i, r := range rs { switch r { case '_': rs[i] = ' ' // Replace underscores with spaces case '.': // Replace dots with spaces unless they are between numeric or space characters if (i != 0 && !IsIsNumericOrSpace(rs[i-1])) || (i != len(rs)-1 && !IsIsNumericOrSpace(rs[i+1])) { rs[i] = ' ' } } } name = string(rs) name = strings.TrimSpace(name) // If the input was non-empty but trimmed to empty, return a single space if len(name) == 0 && origLen > 0 { name = " " } // Convert to uppercase for header formatting return strings.ToUpper(name) } // PadCenter centers a string within a specified width using a padding character. // Extra padding is split between left and right, with slight preference to left if uneven. func PadCenter(s, pad string, width int) string { gap := width - twwidth.Width(s) if gap > 0 { // Calculate left and right padding; ceil ensures left gets extra if gap is odd gapLeft := int(math.Ceil(float64(gap) / 2)) gapRight := gap - gapLeft return strings.Repeat(pad, gapLeft) + s + strings.Repeat(pad, gapRight) } // If no padding needed or string is too wide, return as is return s } // PadRight left-aligns a string within a specified width, filling remaining space on the right with padding. func PadRight(s, pad string, width int) string { gap := width - twwidth.Width(s) if gap > 0 { // Append padding to the right return s + strings.Repeat(pad, gap) } // If no padding needed or string is too wide, return as is return s } // PadLeft right-aligns a string within a specified width, filling remaining space on the left with padding. func PadLeft(s, pad string, width int) string { gap := width - twwidth.Width(s) if gap > 0 { // Prepend padding to the left return strings.Repeat(pad, gap) + s } // If no padding needed or string is too wide, return as is return s } // Pad aligns a string within a specified width using a padding character. // It truncates if the string is wider than the target width. func Pad(s string, padChar string, totalWidth int, alignment Align) string { sDisplayWidth := twwidth.Width(s) if sDisplayWidth > totalWidth { return twwidth.Truncate(s, totalWidth) // Only truncate if necessary } switch alignment { case AlignLeft: return PadRight(s, padChar, totalWidth) case AlignRight: return PadLeft(s, padChar, totalWidth) case AlignCenter: return PadCenter(s, padChar, totalWidth) default: return PadRight(s, padChar, totalWidth) } } // IsIsNumericOrSpace checks if a rune is a digit or space character. // Used in formatting logic to determine safe character replacements. func IsIsNumericOrSpace(r rune) bool { return ('0' <= r && r <= '9') || r == ' ' } // IsNumeric checks if a string represents a valid integer or floating-point number. func IsNumeric(s string) bool { s = strings.TrimSpace(s) if s == "" { return false } // Try parsing as integer first if _, err := strconv.Atoi(s); err == nil { return true } // Then try parsing as float _, err := strconv.ParseFloat(s, 64) return err == nil } // SplitCamelCase splits a camelCase or PascalCase or snake_case string into separate words. // It detects transitions between uppercase, lowercase, digits, and other characters. func SplitCamelCase(src string) (entries []string) { // Validate UTF-8 input; return as single entry if invalid if !utf8.ValidString(src) { return []string{src} } entries = []string{} var runes [][]rune lastClass := 0 class := 0 // Classify each rune into categories: lowercase (1), uppercase (2), digit (3), other (4) for _, r := range src { switch { case unicode.IsLower(r): class = 1 case unicode.IsUpper(r): class = 2 case unicode.IsDigit(r): class = 3 default: class = 4 } // Group consecutive runes of the same class together if class == lastClass { runes[len(runes)-1] = append(runes[len(runes)-1], r) } else { runes = append(runes, []rune{r}) } lastClass = class } // Adjust for cases where an uppercase letter is followed by lowercase (e.g., CamelCase) for i := 0; i < len(runes)-1; i++ { if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) { // Move the last uppercase rune to the next group for proper word splitting runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...) runes[i] = runes[i][:len(runes[i])-1] } } // Convert rune groups to strings, excluding empty, underscore or whitespace-only groups for _, s := range runes { str := string(s) if len(s) > 0 && strings.TrimSpace(str) != "" && str != "_" { entries = append(entries, str) } } return } // Or provides a ternary-like operation for strings, returning 'valid' if cond is true, else 'inValid'. func Or(cond bool, valid, inValid string) string { if cond { return valid } return inValid } // Max returns the greater of two integers. func Max(a, b int) int { if a > b { return a } return b } // Min returns the smaller of two integers. func Min(a, b int) int { if a < b { return a } return b } // BreakPoint finds the rune index where the display width of a string first exceeds the specified limit. // It returns the number of runes if the entire string fits, or 0 if nothing fits. func BreakPoint(s string, limit int) int { // If limit is 0 or negative, nothing can fit if limit <= 0 { return 0 } // Empty string has a breakpoint of 0 if s == "" { return 0 } currentWidth := 0 runeCount := 0 // Iterate over runes, accumulating display width for _, r := range s { runeWidth := twwidth.Width(string(r)) // Calculate width of individual rune if currentWidth+runeWidth > limit { // Adding this rune would exceed the limit; breakpoint is before this rune if currentWidth == 0 { // First rune is too wide; allow breaking after it if limit > 0 if runeWidth > limit && limit > 0 { return 1 } return 0 } return runeCount } currentWidth += runeWidth runeCount++ } // Entire string fits within the limit return runeCount } func MakeAlign(l int, align Align) Alignment { aa := make(Alignment, l) for i := 0; i < l; i++ { aa[i] = align } return aa } tablewriter-1.0.9/tw/fn_test.go000066400000000000000000000016431504170215200164710ustar00rootroot00000000000000package tw import ( "reflect" "testing" ) func TestSplitCase(t *testing.T) { tests := []struct { input string expected []string }{ { input: "", expected: []string{}, }, { input: "snake_Case", expected: []string{"snake", "Case"}, }, { input: "PascalCase", expected: []string{"Pascal", "Case"}, }, { input: "camelCase", expected: []string{"camel", "Case"}, }, { input: "_snake_CasePascalCase_camelCase123", expected: []string{"snake", "Case", "Pascal", "Case", "camel", "Case", "123"}, }, { input: "ㅤ", expected: []string{"ㅤ"}, }, { input: " \r\n\t", expected: []string{}, }, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { if output := SplitCamelCase(tt.input); !reflect.DeepEqual(output, tt.expected) { t.Errorf("SplitCamelCase(%q) = %v, want %v", tt.input, output, tt.expected) } }) } } tablewriter-1.0.9/tw/mapper.go000066400000000000000000000107351504170215200163150ustar00rootroot00000000000000package tw import ( "fmt" "sort" ) // KeyValuePair represents a single key-value pair from a Mapper. type KeyValuePair[K comparable, V any] struct { Key K Value V } // Mapper is a generic map type with comparable keys and any value type. // It provides type-safe operations on maps with additional convenience methods. type Mapper[K comparable, V any] map[K]V // NewMapper creates and returns a new initialized Mapper. func NewMapper[K comparable, V any]() Mapper[K, V] { return make(Mapper[K, V]) } // Get returns the value associated with the key. // If the key doesn't exist or the map is nil, it returns the zero value for the value type. func (m Mapper[K, V]) Get(key K) V { if m == nil { var zero V return zero } return m[key] } // OK returns the value associated with the key and a boolean indicating whether the key exists. func (m Mapper[K, V]) OK(key K) (V, bool) { if m == nil { var zero V return zero, false } val, ok := m[key] return val, ok } // Set sets the value for the specified key. // Does nothing if the map is nil. func (m Mapper[K, V]) Set(key K, value V) Mapper[K, V] { if m != nil { m[key] = value } return m } // Delete removes the specified key from the map. // Does nothing if the key doesn't exist or the map is nil. func (m Mapper[K, V]) Delete(key K) Mapper[K, V] { if m != nil { delete(m, key) } return m } // Has returns true if the key exists in the map, false otherwise. func (m Mapper[K, V]) Has(key K) bool { if m == nil { return false } _, exists := m[key] return exists } // Len returns the number of elements in the map. // Returns 0 if the map is nil. func (m Mapper[K, V]) Len() int { if m == nil { return 0 } return len(m) } // Keys returns a slice containing all keys in the map. // Returns nil if the map is nil or empty. func (m Mapper[K, V]) Keys() []K { if m == nil { return nil } keys := make([]K, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } func (m Mapper[K, V]) Clear() { if m == nil { return } for k := range m { delete(m, k) } } // Values returns a slice containing all values in the map. // Returns nil if the map is nil or empty. func (m Mapper[K, V]) Values() []V { if m == nil { return nil } values := make([]V, 0, len(m)) for _, v := range m { values = append(values, v) } return values } // Each iterates over each key-value pair in the map and calls the provided function. // Does nothing if the map is nil. func (m Mapper[K, V]) Each(fn func(K, V)) { if m != nil { for k, v := range m { fn(k, v) } } } // Filter returns a new Mapper containing only the key-value pairs that satisfy the predicate. func (m Mapper[K, V]) Filter(fn func(K, V) bool) Mapper[K, V] { result := NewMapper[K, V]() if m != nil { for k, v := range m { if fn(k, v) { result[k] = v } } } return result } // MapValues returns a new Mapper with the same keys but values transformed by the provided function. func (m Mapper[K, V]) MapValues(fn func(V) V) Mapper[K, V] { result := NewMapper[K, V]() if m != nil { for k, v := range m { result[k] = fn(v) } } return result } // Clone returns a shallow copy of the Mapper. func (m Mapper[K, V]) Clone() Mapper[K, V] { result := NewMapper[K, V]() if m != nil { for k, v := range m { result[k] = v } } return result } // Slicer converts the Mapper to a Slicer of key-value pairs. func (m Mapper[K, V]) Slicer() Slicer[KeyValuePair[K, V]] { if m == nil { return nil } result := make(Slicer[KeyValuePair[K, V]], 0, len(m)) for k, v := range m { result = append(result, KeyValuePair[K, V]{Key: k, Value: v}) } return result } func (m Mapper[K, V]) SortedKeys() []K { keys := make([]K, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Slice(keys, func(i, j int) bool { a, b := any(keys[i]), any(keys[j]) switch va := a.(type) { case int: if vb, ok := b.(int); ok { return va < vb } case int32: if vb, ok := b.(int32); ok { return va < vb } case int64: if vb, ok := b.(int64); ok { return va < vb } case uint: if vb, ok := b.(uint); ok { return va < vb } case uint64: if vb, ok := b.(uint64); ok { return va < vb } case float32: if vb, ok := b.(float32); ok { return va < vb } case float64: if vb, ok := b.(float64); ok { return va < vb } case string: if vb, ok := b.(string); ok { return va < vb } } // fallback to string comparison return fmt.Sprintf("%v", a) < fmt.Sprintf("%v", b) }) return keys } tablewriter-1.0.9/tw/preset.go000066400000000000000000000014311504170215200163240ustar00rootroot00000000000000package tw // BorderNone defines a border configuration with all sides disabled. var ( // PaddingNone represents explicitly empty padding (no spacing on any side) // Equivalent to Padding{Overwrite: true} PaddingNone = Padding{Left: Empty, Right: Empty, Top: Empty, Bottom: Empty, Overwrite: true} BorderNone = Border{Left: Off, Right: Off, Top: Off, Bottom: Off} LinesNone = Lines{ShowTop: Off, ShowBottom: Off, ShowHeaderLine: Off, ShowFooterLine: Off} SeparatorsNone = Separators{ShowHeader: Off, ShowFooter: Off, BetweenRows: Off, BetweenColumns: Off} ) var ( // PaddingDefault represents standard single-space padding on left/right // Equivalent to Padding{Left: " ", Right: " ", Overwrite: true} PaddingDefault = Padding{Left: " ", Right: " ", Overwrite: true} ) tablewriter-1.0.9/tw/renderer.go000066400000000000000000000126771504170215200166460ustar00rootroot00000000000000package tw import ( "github.com/olekukonko/ll" "io" ) // Renderer defines the interface for rendering tables to an io.Writer. // Implementations must handle headers, rows, footers, and separator lines. type Renderer interface { Start(w io.Writer) error Header(headers [][]string, ctx Formatting) // Renders table header Row(row []string, ctx Formatting) // Renders a single row Footer(footers [][]string, ctx Formatting) // Renders table footer Line(ctx Formatting) // Renders separator line Config() Rendition // Returns renderer config Close() error // Gets Rendition form Blueprint Logger(logger *ll.Logger) // send logger to renderers } // Rendition holds the configuration for the default renderer. type Rendition struct { Borders Border // Border visibility settings Symbols Symbols // Symbols used for table drawing Settings Settings // Rendering behavior settings Streaming bool } // Renditioning has a method to update its rendition. // Let's define an optional interface for this. type Renditioning interface { Rendition(r Rendition) } // Formatting encapsulates the complete formatting context for a table row. // It provides all necessary information to render a row correctly within the table structure. type Formatting struct { Row RowContext // Detailed configuration for the row and its cells Level Level // Hierarchical level (Header, Body, Footer) affecting line drawing HasFooter bool // Indicates if the table includes a footer section IsSubRow bool // Marks this as a continuation or padding line in multi-line rows NormalizedWidths Mapper[int, int] } // CellContext defines the properties and formatting state of an individual table cell. type CellContext struct { Data string // Content to be displayed in the cell, provided by the caller Align Align // Text alignment within the cell (Left, Right, Center, Skip) Padding Padding // Padding characters surrounding the cell content Width int // Suggested width (often overridden by Row.Widths) Merge MergeState // Details about cell spanning across rows or columns } // MergeState captures how a cell merges across different directions. type MergeState struct { Vertical MergeStateOption // Properties for vertical merging (across rows) Horizontal MergeStateOption // Properties for horizontal merging (across columns) Hierarchical MergeStateOption // Properties for nested/hierarchical merging } // MergeStateOption represents common attributes for merging in a specific direction. type MergeStateOption struct { Present bool // True if this merge direction is active Span int // Number of cells this merge spans Start bool // True if this cell is the starting point of the merge End bool // True if this cell is the ending point of the merge } // RowContext manages layout properties and relationships for a row and its columns. // It maintains state about the current row and its neighbors for proper rendering. type RowContext struct { Position Position // Section of the table (Header, Row, Footer) Location Location // Boundary position (First, Middle, End) Current map[int]CellContext // Cells in this row, indexed by column Previous map[int]CellContext // Cells from the row above; nil if none Next map[int]CellContext // Cells from the row below; nil if none Widths Mapper[int, int] // Computed widths for each column ColMaxWidths CellWidth // Maximum allowed width per column } func (r RowContext) GetCell(col int) CellContext { return r.Current[col] } // Separators controls the visibility of separators in the table. type Separators struct { ShowHeader State // Controls header separator visibility ShowFooter State // Controls footer separator visibility BetweenRows State // Determines if lines appear between rows BetweenColumns State // Determines if separators appear between columns } // Lines manages the visibility of table boundary lines. type Lines struct { ShowTop State // Top border visibility ShowBottom State // Bottom border visibility ShowHeaderLine State // Header separator line visibility ShowFooterLine State // Footer separator line visibility } // Settings holds configuration preferences for rendering behavior. type Settings struct { Separators Separators // Separator visibility settings Lines Lines // Line visibility settings CompactMode State // Reserved for future compact rendering (unused) // Cushion State } // Border defines the visibility states of table borders. type Border struct { Left State // Left border visibility Right State // Right border visibility Top State // Top border visibility Bottom State // Bottom border visibility Overwrite bool } type StreamConfig struct { Enable bool // StrictColumns, if true, causes Append() to return an error // in streaming mode if the number of cells in an appended row // does not match the established number of columns for the stream. // If false (default), rows with mismatched column counts will be // padded or truncated with a warning log. StrictColumns bool // Deprecated: Use top-level Config.Widths for streaming width control. // This field will be removed in a future version. It will be respected if // Config.Widths is not set and this field is. Widths CellWidth } tablewriter-1.0.9/tw/slicer.go000066400000000000000000000067201504170215200163110ustar00rootroot00000000000000package tw // Slicer is a generic slice type that provides additional methods for slice manipulation. type Slicer[T any] []T // NewSlicer creates and returns a new initialized Slicer. func NewSlicer[T any]() Slicer[T] { return make(Slicer[T], 0) } // Get returns the element at the specified index. // Returns the zero value if the index is out of bounds or the slice is nil. func (s Slicer[T]) Get(index int) T { if s == nil || index < 0 || index >= len(s) { var zero T return zero } return s[index] } // GetOK returns the element at the specified index and a boolean indicating whether the index was valid. func (s Slicer[T]) GetOK(index int) (T, bool) { if s == nil || index < 0 || index >= len(s) { var zero T return zero, false } return s[index], true } // Append appends elements to the slice and returns the new slice. func (s Slicer[T]) Append(elements ...T) Slicer[T] { return append(s, elements...) } // Prepend adds elements to the beginning of the slice and returns the new slice. func (s Slicer[T]) Prepend(elements ...T) Slicer[T] { return append(elements, s...) } // Len returns the number of elements in the slice. // Returns 0 if the slice is nil. func (s Slicer[T]) Len() int { if s == nil { return 0 } return len(s) } // IsEmpty returns true if the slice is nil or has zero elements. func (s Slicer[T]) IsEmpty() bool { return s.Len() == 0 } // Has returns true if the index exists in the slice. func (s Slicer[T]) Has(index int) bool { return index >= 0 && index < s.Len() } // First returns the first element of the slice, or the zero value if empty. func (s Slicer[T]) First() T { return s.Get(0) } // Last returns the last element of the slice, or the zero value if empty. func (s Slicer[T]) Last() T { return s.Get(s.Len() - 1) } // Each iterates over each element in the slice and calls the provided function. // Does nothing if the slice is nil. func (s Slicer[T]) Each(fn func(T)) { if s != nil { for _, v := range s { fn(v) } } } // Filter returns a new Slicer containing only elements that satisfy the predicate. func (s Slicer[T]) Filter(fn func(T) bool) Slicer[T] { result := NewSlicer[T]() if s != nil { for _, v := range s { if fn(v) { result = result.Append(v) } } } return result } // Map returns a new Slicer with each element transformed by the provided function. func (s Slicer[T]) Map(fn func(T) T) Slicer[T] { result := NewSlicer[T]() if s != nil { for _, v := range s { result = result.Append(fn(v)) } } return result } // Contains returns true if the slice contains an element that satisfies the predicate. func (s Slicer[T]) Contains(fn func(T) bool) bool { if s != nil { for _, v := range s { if fn(v) { return true } } } return false } // Find returns the first element that satisfies the predicate, along with a boolean indicating if it was found. func (s Slicer[T]) Find(fn func(T) bool) (T, bool) { if s != nil { for _, v := range s { if fn(v) { return v, true } } } var zero T return zero, false } // Clone returns a shallow copy of the Slicer. func (s Slicer[T]) Clone() Slicer[T] { result := NewSlicer[T]() if s != nil { result = append(result, s...) } return result } // SlicerToMapper converts a Slicer of KeyValuePair to a Mapper. func SlicerToMapper[K comparable, V any](s Slicer[KeyValuePair[K, V]]) Mapper[K, V] { result := make(Mapper[K, V]) if s == nil { return result } for _, pair := range s { result[pair.Key] = pair.Value } return result } tablewriter-1.0.9/tw/state.go000066400000000000000000000017301504170215200161440ustar00rootroot00000000000000package tw // State represents an on/off state type State int // Public: Methods for State type // Enabled checks if the state is on func (o State) Enabled() bool { return o == Success } // Default checks if the state is unknown func (o State) Default() bool { return o == Unknown } // Disabled checks if the state is off func (o State) Disabled() bool { return o == Fail } // Toggle switches the state between on and off func (o State) Toggle() State { if o == Fail { return Success } return Fail } // Cond executes a condition if the state is enabled func (o State) Cond(c func() bool) bool { if o.Enabled() { return c() } return false } // Or returns this state if enabled, else the provided state func (o State) Or(c State) State { if o.Enabled() { return o } return c } // String returns the string representation of the state func (o State) String() string { if o.Enabled() { return "on" } if o.Disabled() { return "off" } return "undefined" } tablewriter-1.0.9/tw/symbols.go000066400000000000000000000603331504170215200165200ustar00rootroot00000000000000package tw import "fmt" // Symbols defines the interface for table border symbols type Symbols interface { // Name returns the style name Name() string // Basic component symbols Center() string // Junction symbol (where lines cross) Row() string // Horizontal line symbol Column() string // Vertical line symbol // Corner and junction symbols TopLeft() string // LevelHeader-left corner TopMid() string // LevelHeader junction TopRight() string // LevelHeader-right corner MidLeft() string // Left junction MidRight() string // Right junction BottomLeft() string // LevelFooter-left corner BottomMid() string // LevelFooter junction BottomRight() string // LevelFooter-right corner // Optional header-specific symbols HeaderLeft() string HeaderMid() string HeaderRight() string } // BorderStyle defines different border styling options type BorderStyle int // Border style constants const ( StyleNone BorderStyle = iota StyleASCII StyleLight StyleHeavy StyleDouble StyleDoubleLong StyleLightHeavy StyleHeavyLight StyleLightDouble StyleDoubleLight StyleRounded StyleMarkdown StyleGraphical StyleMerger StyleDefault StyleDotted StyleArrow StyleStarry StyleHearts StyleCircuit // Renamed from StyleTech StyleNature StyleArtistic Style8Bit StyleChaos StyleDots StyleBlocks StyleZen StyleVintage StyleSketch StyleArrowDouble StyleCelestial StyleCyber StyleRunic StyleIndustrial StyleInk StyleArcade StyleBlossom StyleFrosted StyleMosaic StyleUFO StyleSteampunk StyleGalaxy StyleJazz StylePuzzle StyleHypno ) // StyleName defines names for border styles type StyleName string func (s StyleName) String() string { return string(s) } const ( StyleNameNothing StyleName = "nothing" StyleNameASCII StyleName = "ascii" StyleNameLight StyleName = "light" StyleNameHeavy StyleName = "heavy" StyleNameDouble StyleName = "double" StyleNameDoubleLong StyleName = "doublelong" StyleNameLightHeavy StyleName = "lightheavy" StyleNameHeavyLight StyleName = "heavylight" StyleNameLightDouble StyleName = "lightdouble" StyleNameDoubleLight StyleName = "doublelight" StyleNameRounded StyleName = "rounded" StyleNameMarkdown StyleName = "markdown" StyleNameGraphical StyleName = "graphical" StyleNameMerger StyleName = "merger" StyleNameDotted StyleName = "dotted" StyleNameArrow StyleName = "arrow" StyleNameStarry StyleName = "starry" StyleNameHearts StyleName = "hearts" StyleNameCircuit StyleName = "circuit" // Renamed from Tech StyleNameNature StyleName = "nature" StyleNameArtistic StyleName = "artistic" StyleName8Bit StyleName = "8bit" StyleNameChaos StyleName = "chaos" StyleNameDots StyleName = "dots" StyleNameBlocks StyleName = "blocks" StyleNameZen StyleName = "zen" StyleNameVintage StyleName = "vintage" StyleNameSketch StyleName = "sketch" StyleNameArrowDouble StyleName = "arrowdouble" StyleNameCelestial StyleName = "celestial" StyleNameCyber StyleName = "cyber" StyleNameRunic StyleName = "runic" StyleNameIndustrial StyleName = "industrial" StyleNameInk StyleName = "ink" StyleNameArcade StyleName = "arcade" StyleNameBlossom StyleName = "blossom" StyleNameFrosted StyleName = "frosted" StyleNameMosaic StyleName = "mosaic" StyleNameUFO StyleName = "ufo" StyleNameSteampunk StyleName = "steampunk" StyleNameGalaxy StyleName = "galaxy" StyleNameJazz StyleName = "jazz" StyleNamePuzzle StyleName = "puzzle" StyleNameHypno StyleName = "hypno" ) // Styles maps BorderStyle to StyleName var Styles = map[BorderStyle]StyleName{ StyleNone: StyleNameNothing, StyleASCII: StyleNameASCII, StyleLight: StyleNameLight, StyleHeavy: StyleNameHeavy, StyleDouble: StyleNameDouble, StyleDoubleLong: StyleNameDoubleLong, StyleLightHeavy: StyleNameLightHeavy, StyleHeavyLight: StyleNameHeavyLight, StyleLightDouble: StyleNameLightDouble, StyleDoubleLight: StyleNameDoubleLight, StyleRounded: StyleNameRounded, StyleMarkdown: StyleNameMarkdown, StyleGraphical: StyleNameGraphical, StyleMerger: StyleNameMerger, StyleDefault: StyleNameLight, StyleDotted: StyleNameDotted, StyleArrow: StyleNameArrow, StyleStarry: StyleNameStarry, StyleHearts: StyleNameHearts, StyleCircuit: StyleNameCircuit, StyleNature: StyleNameNature, StyleArtistic: StyleNameArtistic, Style8Bit: StyleName8Bit, StyleChaos: StyleNameChaos, StyleDots: StyleNameDots, StyleBlocks: StyleNameBlocks, StyleZen: StyleNameZen, StyleVintage: StyleNameVintage, StyleSketch: StyleNameSketch, StyleArrowDouble: StyleNameArrowDouble, StyleCelestial: StyleNameCelestial, StyleCyber: StyleNameCyber, StyleRunic: StyleNameRunic, StyleIndustrial: StyleNameIndustrial, StyleInk: StyleNameInk, StyleArcade: StyleNameArcade, StyleBlossom: StyleNameBlossom, StyleFrosted: StyleNameFrosted, StyleMosaic: StyleNameMosaic, StyleUFO: StyleNameUFO, StyleSteampunk: StyleNameSteampunk, StyleGalaxy: StyleNameGalaxy, StyleJazz: StyleNameJazz, StylePuzzle: StyleNamePuzzle, StyleHypno: StyleNameHypno, } // String returns the string representation of a border style func (s BorderStyle) String() string { return [...]string{ "None", "ASCII", "Light", "Heavy", "Double", "DoubleLong", "LightHeavy", "HeavyLight", "LightDouble", "DoubleLight", "Rounded", "Markdown", "Graphical", "Merger", "Default", "Dotted", "Arrow", "Starry", "Hearts", "Circuit", "Nature", "Artistic", "8Bit", "Chaos", "Dots", "Blocks", "Zen", "Vintage", "Sketch", "ArrowDouble", "Celestial", "Cyber", "Runic", "Industrial", "Ink", "Arcade", "Blossom", "Frosted", "Mosaic", "UFO", "Steampunk", "Galaxy", "Jazz", "Puzzle", "Hypno", }[s] } // NewSymbols creates a new Symbols instance with the specified style func NewSymbols(style BorderStyle) Symbols { switch style { case StyleASCII: return &Glyphs{ name: StyleNameASCII, row: "-", column: "|", center: "+", corners: [9]string{ "+", "+", "+", "+", "+", "+", "+", "+", "+", }, headerLeft: "+", headerMid: "+", headerRight: "+", } case StyleLight, StyleDefault: return &Glyphs{ name: StyleNameLight, row: "─", column: "│", center: "┼", corners: [9]string{ "┌", "┬", "┐", "├", "┼", "┤", "└", "┴", "┘", }, headerLeft: "├", headerMid: "┼", headerRight: "┤", } case StyleHeavy: return &Glyphs{ name: StyleNameHeavy, row: "━", column: "┃", center: "╋", corners: [9]string{ "┏", "┳", "┓", "┣", "╋", "┫", "┗", "┻", "┛", }, headerLeft: "┣", headerMid: "╋", headerRight: "┫", } case StyleDouble: return &Glyphs{ name: StyleNameDouble, row: "═", column: "║", center: "╬", corners: [9]string{ "╔", "╦", "╗", "╠", "╬", "╣", "╚", "╩", "╝", }, headerLeft: "╠", headerMid: "╬", headerRight: "╣", } case StyleDoubleLong: return &Glyphs{ name: StyleNameDoubleLong, row: "═╡═", column: "╞", center: "╪", corners: [9]string{ "╔═╡", "═╤═", "╡═╗", "╟ ", "╪ ", " ╢", "╚═╡", "═╧═", "╡═╝", }, headerLeft: "╟═╡", headerMid: "╪═╡", headerRight: "╡═╢", } case StyleLightHeavy: return &Glyphs{ name: StyleNameLightHeavy, row: "─", column: "┃", center: "╂", corners: [9]string{ "┍", "┯", "┑", "┝", "╂", "┥", "┕", "┷", "┙", }, headerLeft: "┝", headerMid: "╂", headerRight: "┥", } case StyleHeavyLight: return &Glyphs{ name: StyleNameHeavyLight, row: "━", column: "│", center: "┿", corners: [9]string{ "┎", "┰", "┒", "┠", "┿", "┨", "┖", "┸", "┚", }, headerLeft: "┠", headerMid: "┿", headerRight: "┨", } case StyleLightDouble: return &Glyphs{ name: StyleNameLightDouble, row: "─", column: "║", center: "╫", corners: [9]string{ "╓", "╥", "╖", "╟", "╫", "╢", "╙", "╨", "╜", }, headerLeft: "╟", headerMid: "╫", headerRight: "╢", } case StyleDoubleLight: return &Glyphs{ name: StyleNameDoubleLight, row: "═", column: "│", center: "╪", corners: [9]string{ "╒", "╤", "╕", "╞", "╪", "╡", "╘", "╧", "╛", }, headerLeft: "╞", headerMid: "╪", headerRight: "╡", } case StyleRounded: return &Glyphs{ name: StyleNameRounded, row: "─", column: "│", center: "┼", corners: [9]string{ "╭", "┬", "╮", "├", "┼", "┤", "╰", "┴", "╯", }, headerLeft: "├", headerMid: "┼", headerRight: "┤", } case StyleMarkdown: return &Glyphs{ name: StyleNameMarkdown, row: "-", column: "|", center: "|", corners: [9]string{ "", "", "", "|", "|", "|", "", "", "", }, headerLeft: "|", headerMid: "|", headerRight: "|", } case StyleGraphical: return &Glyphs{ name: StyleNameGraphical, row: "┄┄", column: "┆", center: "╂", corners: [9]string{ "┌┄", "┄┄", "┄┐", "┆ ", "╂ ", " ┆", "└┄", "┄┄", "┄┘", }, headerLeft: "├┄", headerMid: "╂┄", headerRight: "┄┤", } case StyleMerger: return &Glyphs{ name: StyleNameMerger, row: "─", column: "│", center: "+", corners: [9]string{ "┌", "┬", "┐", "├", "┼", "┤", "└", "┴", "┘", }, headerLeft: "├", headerMid: "+", headerRight: "┤", } case StyleDotted: return &Glyphs{ name: StyleNameDotted, row: "·", column: ":", center: "+", corners: [9]string{ ".", "·", ".", ":", "+", ":", "'", "·", "'", }, headerLeft: ":", headerMid: "+", headerRight: ":", } case StyleArrow: return &Glyphs{ name: StyleNameArrow, row: "→", column: "↓", center: "↔", corners: [9]string{ "↗", "↑", "↖", "→", "↔", "←", "↘", "↓", "↙", }, headerLeft: "→", headerMid: "↔", headerRight: "←", } case StyleStarry: return &Glyphs{ name: StyleNameStarry, row: "★", column: "☆", center: "✶", corners: [9]string{ "✧", "✯", "✧", "✦", "✶", "✦", "✧", "✯", "✧", }, headerLeft: "✦", headerMid: "✶", headerRight: "✦", } case StyleHearts: return &Glyphs{ name: StyleNameHearts, row: "♥", column: "❤", center: "✚", corners: [9]string{ "❥", "♡", "❥", "❣", "✚", "❣", "❦", "♡", "❦", }, headerLeft: "❣", headerMid: "✚", headerRight: "❣", } case StyleCircuit: return &Glyphs{ name: StyleNameCircuit, row: "=", column: "||", center: "<>", corners: [9]string{ "/*", "##", "*/", "//", "<>", "\\", "\\*", "##", "*/", }, headerLeft: "//", headerMid: "<>", headerRight: "\\", } case StyleNature: return &Glyphs{ name: StyleNameNature, row: "~", column: "|", center: "❀", corners: [9]string{ "🌱", "🌿", "🌱", "🍃", "❀", "🍃", "🌻", "🌾", "🌻", }, headerLeft: "🍃", headerMid: "❀", headerRight: "🍃", } case StyleArtistic: return &Glyphs{ name: StyleNameArtistic, row: "▬", column: "▐", center: "⬔", corners: [9]string{ "◈", "◊", "◈", "◀", "⬔", "▶", "◭", "▣", "◮", }, headerLeft: "◀", headerMid: "⬔", headerRight: "▶", } case Style8Bit: return &Glyphs{ name: StyleName8Bit, row: "■", column: "█", center: "♦", corners: [9]string{ "╔", "▲", "╗", "◄", "♦", "►", "╚", "▼", "╝", }, headerLeft: "◄", headerMid: "♦", headerRight: "►", } case StyleChaos: return &Glyphs{ name: StyleNameChaos, row: "≈", column: "§", center: "☯", corners: [9]string{ "⌘", "∞", "⌥", "⚡", "☯", "♞", "⌂", "∆", "◊", }, headerLeft: "⚡", headerMid: "☯", headerRight: "♞", } case StyleDots: return &Glyphs{ name: StyleNameDots, row: "·", column: " ", center: "·", corners: [9]string{ "·", "·", "·", " ", "·", " ", "·", "·", "·", }, headerLeft: " ", headerMid: "·", headerRight: " ", } case StyleBlocks: return &Glyphs{ name: StyleNameBlocks, row: "▀", column: "█", center: "█", corners: [9]string{ "▛", "▀", "▜", "▌", "█", "▐", "▙", "▄", "▟", }, headerLeft: "▌", headerMid: "█", headerRight: "▐", } case StyleZen: return &Glyphs{ name: StyleNameZen, row: "~", column: " ", center: "☯", corners: [9]string{ " ", "♨", " ", " ", "☯", " ", " ", "♨", " ", }, headerLeft: " ", headerMid: "☯", headerRight: " ", } case StyleVintage: return &Glyphs{ name: StyleNameVintage, row: "────", column: " ⁜ ", center: " ✠ ", corners: [9]string{ "╔══", "══╤", "══╗", " ⁜ ", " ✠ ", " ⁜ ", "╚══", "══╧", "══╝", }, headerLeft: " ├─", headerMid: "─✠─", headerRight: "─┤ ", } case StyleSketch: return &Glyphs{ name: StyleNameSketch, row: "~~", column: "/", center: "+", corners: [9]string{ " .", "~~", ". ", "/ ", "+ ", " \\", " '", "~~", "` ", }, headerLeft: "/~", headerMid: "+~", headerRight: "~\\", } case StyleArrowDouble: return &Glyphs{ name: StyleNameArrowDouble, row: "»»", column: "⫸", center: "✿", corners: [9]string{ "⌜»", "»»", "»⌝", "⫸ ", "✿ ", " ⫷", "⌞»", "»»", "»⌟", }, headerLeft: "⫸»", headerMid: "✿»", headerRight: "»⫷", } case StyleCelestial: return &Glyphs{ name: StyleNameCelestial, row: "✦✧", column: "☽", center: "☀", corners: [9]string{ "✧✦", "✦✧", "✦✧", "☽ ", "☀ ", " ☾", "✧✦", "✦✧", "✦✧", }, headerLeft: "☽✦", headerMid: "☀✧", headerRight: "✦☾", } case StyleCyber: return &Glyphs{ name: StyleNameCyber, row: "═╦═", column: "║", center: "╬", corners: [9]string{ "╔╦═", "╦═╦", "═╦╗", "║ ", "╬ ", " ║", "╚╩═", "╩═╩", "═╩╝", }, headerLeft: "╠╦═", headerMid: "╬═╦", headerRight: "═╦╣", } case StyleRunic: return &Glyphs{ name: StyleNameRunic, row: "ᛖᛖᛖ", column: "ᛟ", center: "ᛞ", corners: [9]string{ "ᛏᛖᛖ", "ᛖᛖᛖ", "ᛖᛖᛏ", "ᛟ ", "ᛞ ", " ᛟ", "ᛗᛖᛖ", "ᛖᛖᛖ", "ᛖᛖᛗ", }, headerLeft: "ᛟᛖᛖ", headerMid: "ᛞᛖᛖ", headerRight: "ᛖᛖᛟ", } case StyleIndustrial: return &Glyphs{ name: StyleNameIndustrial, row: "━╋━", column: "┃", center: "╋", corners: [9]string{ "┏╋━", "╋━╋", "━╋┓", "┃ ", "╋ ", " ┃", "┗╋━", "╋━╋", "━╋┛", }, headerLeft: "┣╋━", headerMid: "╋━╋", headerRight: "━╋┫", } case StyleInk: return &Glyphs{ name: StyleNameInk, row: "﹌", column: "︱", center: "✒", corners: [9]string{ "﹏", "﹌", "﹏", "︱ ", "✒ ", " ︱", "﹋", "﹌", "﹋", }, headerLeft: "︱﹌", headerMid: "✒﹌", headerRight: "﹌︱", } case StyleArcade: return &Glyphs{ name: StyleNameArcade, row: "■□", column: "▐", center: "◉", corners: [9]string{ "▞■", "■□", "□▚", "▐ ", "◉ ", " ▐", "▚■", "■□", "□▞", }, headerLeft: "▐■", headerMid: "◉□", headerRight: "■▐", } case StyleBlossom: return &Glyphs{ name: StyleNameBlossom, row: "🌸", column: "🌿", center: "✿", corners: [9]string{ "🌷", "🌸", "🌷", "🌿", "✿", "🌿", "🌱", "🌸", "🌱", }, headerLeft: "🌿🌸", headerMid: "✿🌸", headerRight: "🌸🌿", } case StyleFrosted: return &Glyphs{ name: StyleNameFrosted, row: "░▒░", column: "▓", center: "◍", corners: [9]string{ "◌░▒", "░▒░", "▒░◌", "▓ ", "◍ ", " ▓", "◌░▒", "░▒░", "▒░◌", }, headerLeft: "▓░▒", headerMid: "◍▒░", headerRight: "░▒▓", } case StyleMosaic: return &Glyphs{ name: StyleNameMosaic, row: "▰▱", column: "⧉", center: "⬖", corners: [9]string{ "⧠▰", "▰▱", "▱⧠", "⧉ ", "⬖ ", " ⧉", "⧅▰", "▰▱", "▱⧅", }, headerLeft: "⧉▰", headerMid: "⬖▱", headerRight: "▰⧉", } case StyleUFO: return &Glyphs{ name: StyleNameUFO, row: "⊚⊚", column: "☽", center: "☢", corners: [9]string{ "⌖⊚", "⊚⊚", "⊚⌖", "☽ ", "☢ ", " ☽", "⌗⊚", "⊚⊚", "⊚⌗", }, headerLeft: "☽⊚", headerMid: "☢⊚", headerRight: "⊚☽", } case StyleSteampunk: return &Glyphs{ name: StyleNameSteampunk, row: "═⚙═", column: "⛓️", center: "⚔️", corners: [9]string{ "🜂⚙═", "═⚙═", "═⚙🜂", "⛓️ ", "⚔️ ", " ⛓️", "🜄⚙═", "═⚙═", "═⚙🜄", }, headerLeft: "⛓️⚙═", headerMid: "⚔️═⚙", headerRight: "═⚙⛓️", } case StyleGalaxy: return &Glyphs{ name: StyleNameGalaxy, row: "≋≋", column: "♆", center: "☄️", corners: [9]string{ "⌇≋", "≋≋", "≋⌇", "♆ ", "☄️ ", " ♆", "⌇≋", "≋≋", "≋⌇", }, headerLeft: "♆≋", headerMid: "☄️≋", headerRight: "≋♆", } case StyleJazz: return &Glyphs{ name: StyleNameJazz, row: "♬♬", column: "▷", center: "★", corners: [9]string{ "♔♬", "♬♬", "♬♔", "▷ ", "★ ", " ◁", "♕♬", "♬♬", "♬♕", }, headerLeft: "▷♬", headerMid: "★♬", headerRight: "♬◁", } case StylePuzzle: return &Glyphs{ name: StyleNamePuzzle, row: "▣▣", column: "◫", center: "✚", corners: [9]string{ "◩▣", "▣▣", "▣◪", "◫ ", "✚ ", " ◫", "◧▣", "▣▣", "▣◨", }, headerLeft: "◫▣", headerMid: "✚▣", headerRight: "▣◫", } case StyleHypno: return &Glyphs{ name: StyleNameHypno, row: "◜◝", column: "꩜", center: "⃰", corners: [9]string{ "◟◜", "◜◝", "◝◞", "꩜ ", "⃰ ", " ꩜", "◟◜", "◜◝", "◝◞", }, headerLeft: "꩜◜", headerMid: "⃰◝", headerRight: "◜꩜", } default: return &Glyphs{ name: StyleNameNothing, row: "", column: "", center: "", corners: [9]string{ "", "", "", "", "", "", "", "", "", }, headerLeft: "", headerMid: "", headerRight: "", } } } // SymbolCustom implements the Symbols interface with fully configurable symbols type SymbolCustom struct { name string center string row string column string topLeft string topMid string topRight string midLeft string midRight string bottomLeft string bottomMid string bottomRight string headerLeft string headerMid string headerRight string } // NewSymbolCustom creates a new customizable border style func NewSymbolCustom(name string) *SymbolCustom { return &SymbolCustom{ name: name, center: "+", row: "-", column: "|", } } // Implement all Symbols interface methods func (c *SymbolCustom) Name() string { return c.name } func (c *SymbolCustom) Center() string { return c.center } func (c *SymbolCustom) Row() string { return c.row } func (c *SymbolCustom) Column() string { return c.column } func (c *SymbolCustom) TopLeft() string { return c.topLeft } func (c *SymbolCustom) TopMid() string { return c.topMid } func (c *SymbolCustom) TopRight() string { return c.topRight } func (c *SymbolCustom) MidLeft() string { return c.midLeft } func (c *SymbolCustom) MidRight() string { return c.midRight } func (c *SymbolCustom) BottomLeft() string { return c.bottomLeft } func (c *SymbolCustom) BottomMid() string { return c.bottomMid } func (c *SymbolCustom) BottomRight() string { return c.bottomRight } func (c *SymbolCustom) HeaderLeft() string { return c.headerLeft } func (c *SymbolCustom) HeaderMid() string { return c.headerMid } func (c *SymbolCustom) HeaderRight() string { return c.headerRight } // Builder methods for fluent configuration func (c *SymbolCustom) WithCenter(s string) *SymbolCustom { c.center = s; return c } func (c *SymbolCustom) WithRow(s string) *SymbolCustom { c.row = s; return c } func (c *SymbolCustom) WithColumn(s string) *SymbolCustom { c.column = s; return c } func (c *SymbolCustom) WithTopLeft(s string) *SymbolCustom { c.topLeft = s; return c } func (c *SymbolCustom) WithTopMid(s string) *SymbolCustom { c.topMid = s; return c } func (c *SymbolCustom) WithTopRight(s string) *SymbolCustom { c.topRight = s; return c } func (c *SymbolCustom) WithMidLeft(s string) *SymbolCustom { c.midLeft = s; return c } func (c *SymbolCustom) WithMidRight(s string) *SymbolCustom { c.midRight = s; return c } func (c *SymbolCustom) WithBottomLeft(s string) *SymbolCustom { c.bottomLeft = s; return c } func (c *SymbolCustom) WithBottomMid(s string) *SymbolCustom { c.bottomMid = s; return c } func (c *SymbolCustom) WithBottomRight(s string) *SymbolCustom { c.bottomRight = s; return c } func (c *SymbolCustom) WithHeaderLeft(s string) *SymbolCustom { c.headerLeft = s; return c } func (c *SymbolCustom) WithHeaderMid(s string) *SymbolCustom { c.headerMid = s; return c } func (c *SymbolCustom) WithHeaderRight(s string) *SymbolCustom { c.headerRight = s; return c } // Preview renders a small sample table to visualize the border style func (s *SymbolCustom) Preview() string { return fmt.Sprintf( "%s%s%s\n%s %s %s\n%s%s%s", s.TopLeft(), s.Row(), s.TopRight(), s.Column(), s.Center(), s.Column(), s.BottomLeft(), s.Row(), s.BottomRight(), ) } // Glyphs provides fully independent border symbols with a corners array type Glyphs struct { name StyleName row string column string center string corners [9]string // [TopLeft, TopMid, TopRight, MidLeft, Center, MidRight, BottomLeft, BottomMid, BottomRight] headerLeft string headerMid string headerRight string } // Glyphs symbol methods func (s *Glyphs) Name() string { return s.name.String() } func (s *Glyphs) Center() string { return s.center } func (s *Glyphs) Row() string { return s.row } func (s *Glyphs) Column() string { return s.column } func (s *Glyphs) TopLeft() string { return s.corners[0] } func (s *Glyphs) TopMid() string { return s.corners[1] } func (s *Glyphs) TopRight() string { return s.corners[2] } func (s *Glyphs) MidLeft() string { return s.corners[3] } func (s *Glyphs) MidRight() string { return s.corners[5] } func (s *Glyphs) BottomLeft() string { return s.corners[6] } func (s *Glyphs) BottomMid() string { return s.corners[7] } func (s *Glyphs) BottomRight() string { return s.corners[8] } func (s *Glyphs) HeaderLeft() string { return s.headerLeft } func (s *Glyphs) HeaderMid() string { return s.headerMid } func (s *Glyphs) HeaderRight() string { return s.headerRight } // Preview renders a small sample table to visualize the border style func (s *Glyphs) Preview() string { return fmt.Sprintf( "%s%s%s\n%s %s %s\n%s%s%s", s.TopLeft(), s.Row(), s.TopRight(), s.Column(), s.Center(), s.Column(), s.BottomLeft(), s.Row(), s.BottomRight(), ) } tablewriter-1.0.9/tw/tw.go000066400000000000000000000051111504170215200154530ustar00rootroot00000000000000package tw // Operation Status Constants // Used to indicate the success or failure of operations const ( Pending = 0 // Operation failed Fail = -1 // Operation failed Success = 1 // Operation succeeded MinimumColumnWidth = 8 ) const ( Empty = "" Skip = "" Space = " " NewLine = "\n" Column = ":" Dash = "-" ) // Feature State Constants // Represents enabled/disabled states for features const ( Unknown State = Pending // Feature is enabled On State = Success // Feature is enabled Off State = Fail // Feature is disabled ) // Table Alignment Constants // Defines text alignment options for table content const ( AlignNone Align = "none" // Center-aligned text AlignCenter Align = "center" // Center-aligned text AlignRight Align = "right" // Right-aligned text AlignLeft Align = "left" // Left-aligned text AlignDefault = AlignLeft // Left-aligned text ) const ( Header Position = "header" // Table header section Row Position = "row" // Table row section Footer Position = "footer" // Table footer section ) const ( LevelHeader Level = iota // Topmost line position LevelBody // LevelBody line position LevelFooter // LevelFooter line position ) const ( LocationFirst Location = "first" // Topmost line position LocationMiddle Location = "middle" // LevelBody line position LocationEnd Location = "end" // LevelFooter line position ) const ( SectionHeader = "header" SectionRow = "row" SectionFooter = "footer" ) // Text Wrapping Constants // Defines text wrapping behavior in table cells const ( WrapNone = iota // No wrapping WrapNormal // Standard word wrapping WrapTruncate // Truncate text with ellipsis WrapBreak // Break words to fit ) // Cell Merge Constants // Specifies cell merging behavior in tables const ( MergeNone = iota // No merging MergeVertical // Merge cells vertically MergeHorizontal // Merge cells horizontally MergeBoth // Merge both vertically and horizontally MergeHierarchical // Hierarchical merging ) // Special Character Constants // Defines special characters used in formatting const ( CharEllipsis = "…" // Ellipsis character for truncation CharBreak = "↩" // Break character for wrapping ) type Spot int const ( SpotNone Spot = iota SpotTopLeft SpotTopCenter SpotTopRight SpotBottomLeft SpotBottomCenter // Default for legacy SetCaption SpotBottomRight SpotLeftTop SpotLeftCenter SpotLeftBottom SpotRightTop SpotRightCenter SpotRIghtBottom ) tablewriter-1.0.9/tw/types.go000066400000000000000000000146351504170215200162000ustar00rootroot00000000000000// Package tw defines types and constants for table formatting and configuration, // including validation logic for various table properties. package tw import ( "fmt" "github.com/olekukonko/errors" "strings" ) // Custom error handling library // Position defines where formatting applies in the table (e.g., header, footer, or rows). type Position string // Validate checks if the Position is one of the allowed values: Header, Footer, or Row. func (pos Position) Validate() error { switch pos { case Header, Footer, Row: return nil // Valid position } // Return an error for any unrecognized position return errors.New("invalid position") } // Filter defines a function type for processing cell content. // It takes a slice of strings (representing cell data) and returns a processed slice. type Filter func([]string) []string // Formatter defines an interface for types that can format themselves into a string. // Used for custom formatting of table cell content. type Formatter interface { Format() string // Returns the formatted string representation } // Align specifies the text alignment within a table cell. type Align string // Validate checks if the Align is one of the allowed values: None, Center, Left, or Right. func (a Align) Validate() error { switch a { case AlignNone, AlignCenter, AlignLeft, AlignRight: return nil // Valid alignment } // Return an error for any unrecognized alignment return errors.New("invalid align") } type Alignment []Align func (a Alignment) String() string { var str strings.Builder for i, a := range a { if i > 0 { str.WriteString("; ") } str.WriteString(fmt.Sprint(i)) str.WriteString("=") str.WriteString(string(a)) } return str.String() } func (a Alignment) Add(aligns ...Align) Alignment { aa := make(Alignment, len(aligns)) copy(aa, aligns) return aa } func (a Alignment) Set(col int, align Align) Alignment { if col >= 0 && col < len(a) { a[col] = align } return a } // Copy creates a new independent copy of the Alignment func (a Alignment) Copy() Alignment { aa := make(Alignment, len(a)) copy(aa, a) return aa } // Level indicates the vertical position of a line in the table (e.g., header, body, or footer). type Level int // Validate checks if the Level is one of the allowed values: Header, Body, or Footer. func (l Level) Validate() error { switch l { case LevelHeader, LevelBody, LevelFooter: return nil // Valid level } // Return an error for any unrecognized level return errors.New("invalid level") } // Location specifies the horizontal position of a cell or column within a table row. type Location string // Validate checks if the Location is one of the allowed values: First, Middle, or End. func (l Location) Validate() error { switch l { case LocationFirst, LocationMiddle, LocationEnd: return nil // Valid location } // Return an error for any unrecognized location return errors.New("invalid location") } type Caption struct { Text string Spot Spot Align Align Width int } func (c Caption) WithText(text string) Caption { c.Text = text return c } func (c Caption) WithSpot(spot Spot) Caption { c.Spot = spot return c } func (c Caption) WithAlign(align Align) Caption { c.Align = align return c } func (c Caption) WithWidth(width int) Caption { c.Width = width return c } type Control struct { Hide State } // Compact configures compact width optimization for merged cells. type Compact struct { Merge State // Merge enables compact width calculation during cell merging, optimizing space allocation. } // Struct holds settings for struct-based operations like AutoHeader. type Struct struct { // AutoHeader automatically extracts and sets headers from struct fields when Bulk is called with a slice of structs. // Uses JSON tags if present, falls back to field names (title-cased). Skips unexported or json:"-" fields. // Enabled by default for convenience. AutoHeader State // Tags is a priority-ordered list of struct tag keys to check for header names. // The first tag found on a field will be used. Defaults to ["json", "db"]. Tags []string } // Behavior defines settings that control table rendering behaviors, such as column visibility and content formatting. type Behavior struct { AutoHide State // AutoHide determines whether empty columns are hidden. Ignored in streaming mode. TrimSpace State // TrimSpace enables trimming of leading and trailing spaces from cell content. Header Control // Header specifies control settings for the table header. Footer Control // Footer specifies control settings for the table footer. // Compact enables optimized width calculation for merged cells, such as in horizontal merges, // by systematically determining the most efficient width instead of scaling by the number of columns. Compact Compact // Structs contains settings for how struct data is processed. Structs Struct } // Padding defines the spacing characters around cell content in all four directions. // A zero-value Padding struct will use the table's default padding unless Overwrite is true. type Padding struct { Left string Right string Top string Bottom string // Overwrite forces tablewriter to use this padding configuration exactly as specified, // even when empty. When false (default), empty Padding fields will inherit defaults. // // For explicit no-padding, use the PaddingNone constant instead of setting Overwrite. Overwrite bool } // Common padding configurations for convenience // Equals reports whether two Padding configurations are identical in all fields. // This includes comparing the Overwrite flag as part of the equality check. func (p Padding) Equals(padding Padding) bool { return p.Left == padding.Left && p.Right == padding.Right && p.Top == padding.Top && p.Bottom == padding.Bottom && p.Overwrite == padding.Overwrite } // Empty reports whether all padding strings are empty (all fields == ""). // Note that an Empty padding may still take effect if Overwrite is true. func (p Padding) Empty() bool { return p.Left == "" && p.Right == "" && p.Top == "" && p.Bottom == "" } // Paddable reports whether this Padding configuration should override existing padding. // Returns true if either: // - Any padding string is non-empty (!p.Empty()) // - Overwrite flag is true (even with all strings empty) // // This is used internally during configuration merging to determine whether to // apply the padding settings. func (p Padding) Paddable() bool { return !p.Empty() || p.Overwrite } tablewriter-1.0.9/zoo.go000066400000000000000000001713711504170215200152120ustar00rootroot00000000000000package tablewriter import ( "database/sql" "fmt" "github.com/olekukonko/errors" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" "io" "math" "reflect" "strconv" "strings" ) // applyHierarchicalMerges applies hierarchical merges to row content. // Parameters ctx and mctx hold rendering and merge state. // No return value. func (t *Table) applyHierarchicalMerges(ctx *renderContext, mctx *mergeContext) { ctx.logger.Debug("Applying hierarchical merges (left-to-right vertical flow - snapshot comparison)") if len(ctx.rowLines) <= 1 { ctx.logger.Debug("Skipping hierarchical merges - less than 2 rows") return } numCols := ctx.numCols originalRowLines := make([][][]string, len(ctx.rowLines)) for i, row := range ctx.rowLines { originalRowLines[i] = make([][]string, len(row)) for j, line := range row { originalRowLines[i][j] = make([]string, len(line)) copy(originalRowLines[i][j], line) } } ctx.logger.Debug("Created snapshot of original row data for hierarchical merge comparison.") hMergeStartRow := make(map[int]int) for r := 1; r < len(ctx.rowLines); r++ { leftCellContinuedHierarchical := false for c := 0; c < numCols; c++ { if mctx.rowMerges[r] == nil { mctx.rowMerges[r] = make(map[int]tw.MergeState) } if mctx.rowMerges[r-1] == nil { mctx.rowMerges[r-1] = make(map[int]tw.MergeState) } canCompare := r > 0 && len(originalRowLines[r]) > 0 && len(originalRowLines[r-1]) > 0 if !canCompare { currentState := mctx.rowMerges[r][c] currentState.Hierarchical = tw.MergeStateOption{} mctx.rowMerges[r][c] = currentState ctx.logger.Debugf("HCompare Skipped: r=%d, c=%d - Insufficient data in snapshot", r, c) leftCellContinuedHierarchical = false continue } // Join all lines of the cell for comparison var currentVal, aboveVal string for _, line := range originalRowLines[r] { if c < len(line) { currentVal += line[c] } } for _, line := range originalRowLines[r-1] { if c < len(line) { aboveVal += line[c] } } currentVal = t.Trimmer(currentVal) aboveVal = t.Trimmer(aboveVal) currentState := mctx.rowMerges[r][c] prevStateAbove := mctx.rowMerges[r-1][c] valuesMatch := currentVal == aboveVal && currentVal != "" && currentVal != "-" hierarchyAllowed := c == 0 || leftCellContinuedHierarchical shouldContinue := valuesMatch && hierarchyAllowed ctx.logger.Debugf("HCompare: r=%d, c=%d; current='%s', above='%s'; match=%v; leftCont=%v; shouldCont=%v", r, c, currentVal, aboveVal, valuesMatch, leftCellContinuedHierarchical, shouldContinue) if shouldContinue { currentState.Hierarchical.Present = true currentState.Hierarchical.Start = false if prevStateAbove.Hierarchical.Present && !prevStateAbove.Hierarchical.End { startRow, ok := hMergeStartRow[c] if !ok { ctx.logger.Debugf("Hierarchical merge WARNING: Recovering lost start row at r=%d, c=%d. Assuming r-1 was start.", r, c) startRow = r - 1 hMergeStartRow[c] = startRow startState := mctx.rowMerges[startRow][c] startState.Hierarchical.Present = true startState.Hierarchical.Start = true startState.Hierarchical.End = false mctx.rowMerges[startRow][c] = startState } ctx.logger.Debugf("Hierarchical merge CONTINUED row %d, col %d. Block previously started row %d", r, c, startRow) } else { startRow := r - 1 hMergeStartRow[c] = startRow startState := mctx.rowMerges[startRow][c] startState.Hierarchical.Present = true startState.Hierarchical.Start = true startState.Hierarchical.End = false mctx.rowMerges[startRow][c] = startState ctx.logger.Debugf("Hierarchical merge START detected for block ending at or after row %d, col %d (started at row %d)", r, c, startRow) } for lineIdx := range ctx.rowLines[r] { if c < len(ctx.rowLines[r][lineIdx]) { ctx.rowLines[r][lineIdx][c] = tw.Empty } } leftCellContinuedHierarchical = true } else { currentState.Hierarchical = tw.MergeStateOption{} if startRow, ok := hMergeStartRow[c]; ok { t.finalizeHierarchicalMergeBlock(ctx, mctx, c, startRow, r-1) delete(hMergeStartRow, c) } leftCellContinuedHierarchical = false } mctx.rowMerges[r][c] = currentState } } lastRowIdx := len(ctx.rowLines) - 1 if lastRowIdx >= 0 { for c, startRow := range hMergeStartRow { t.finalizeHierarchicalMergeBlock(ctx, mctx, c, startRow, lastRowIdx) } } ctx.logger.Debug("Hierarchical merge processing completed") } // applyHorizontalMergeWidths adjusts column widths for horizontal merges. // Parameters include position, ctx for rendering, and mergeStates for merges. // No return value. func (t *Table) applyHorizontalMergeWidths(position tw.Position, ctx *renderContext, mergeStates map[int]tw.MergeState) { if mergeStates == nil { t.logger.Debugf("applyHorizontalMergeWidths: Skipping %s - no merge states", position) return } t.logger.Debugf("applyHorizontalMergeWidths: Applying HMerge width recalc for %s", position) numCols := ctx.numCols targetWidthsMap := ctx.widths[position] originalNormalizedWidths := tw.NewMapper[int, int]() for i := 0; i < numCols; i++ { originalNormalizedWidths.Set(i, targetWidthsMap.Get(i)) } separatorWidth := 0 if t.renderer != nil { rendererConfig := t.renderer.Config() if rendererConfig.Settings.Separators.BetweenColumns.Enabled() { separatorWidth = twwidth.Width(rendererConfig.Symbols.Column()) } } processedCols := make(map[int]bool) for col := 0; col < numCols; col++ { if processedCols[col] { continue } state, exists := mergeStates[col] if !exists { continue } if state.Horizontal.Present && state.Horizontal.Start { totalWidth := 0 span := state.Horizontal.Span t.logger.Debugf(" -> HMerge detected: startCol=%d, span=%d, separatorWidth=%d", col, span, separatorWidth) for i := 0; i < span && (col+i) < numCols; i++ { currentColIndex := col + i normalizedWidth := originalNormalizedWidths.Get(currentColIndex) totalWidth += normalizedWidth t.logger.Debugf(" -> col %d: adding normalized width %d", currentColIndex, normalizedWidth) if i > 0 && separatorWidth > 0 { totalWidth += separatorWidth t.logger.Debugf(" -> col %d: adding separator width %d", currentColIndex, separatorWidth) } } targetWidthsMap.Set(col, totalWidth) t.logger.Debugf(" -> Set %s col %d width to %d (merged)", position, col, totalWidth) processedCols[col] = true for i := 1; i < span && (col+i) < numCols; i++ { targetWidthsMap.Set(col+i, 0) t.logger.Debugf(" -> Set %s col %d width to 0 (part of merge)", position, col+i) processedCols[col+i] = true } } } ctx.logger.Debugf("applyHorizontalMergeWidths: Final widths for %s: %v", position, targetWidthsMap) } // applyVerticalMerges applies vertical merges to row content. // Parameters ctx and mctx hold rendering and merge state. // No return value. func (t *Table) applyVerticalMerges(ctx *renderContext, mctx *mergeContext) { ctx.logger.Debugf("Applying vertical merges across %d rows", len(ctx.rowLines)) numCols := ctx.numCols mergeStartRow := make(map[int]int) mergeStartContent := make(map[int]string) for i := 0; i < len(ctx.rowLines); i++ { if i >= len(mctx.rowMerges) { newRowMerges := make([]map[int]tw.MergeState, i+1) copy(newRowMerges, mctx.rowMerges) for k := len(mctx.rowMerges); k <= i; k++ { newRowMerges[k] = make(map[int]tw.MergeState) } mctx.rowMerges = newRowMerges ctx.logger.Debugf("Extended rowMerges to index %d", i) } else if mctx.rowMerges[i] == nil { mctx.rowMerges[i] = make(map[int]tw.MergeState) } if len(ctx.rowLines[i]) == 0 { continue } currentLineContent := ctx.rowLines[i] for col := 0; col < numCols; col++ { // Join all lines of the cell to compare full content var currentVal strings.Builder for _, line := range currentLineContent { if col < len(line) { currentVal.WriteString(line[col]) } } currentValStr := t.Trimmer(currentVal.String()) startRow, ongoingMerge := mergeStartRow[col] startContent := mergeStartContent[col] mergeState := mctx.rowMerges[i][col] if ongoingMerge && currentValStr == startContent && currentValStr != "" { mergeState.Vertical = tw.MergeStateOption{ Present: true, Span: 0, Start: false, End: false, } mctx.rowMerges[i][col] = mergeState for lineIdx := range ctx.rowLines[i] { if col < len(ctx.rowLines[i][lineIdx]) { ctx.rowLines[i][lineIdx][col] = tw.Empty } } ctx.logger.Debugf("Vertical merge continued at row %d, col %d", i, col) } else { if ongoingMerge { endedRow := i - 1 if endedRow >= 0 && endedRow >= startRow { startState := mctx.rowMerges[startRow][col] startState.Vertical.Span = (endedRow - startRow) + 1 startState.Vertical.End = startState.Vertical.Span == 1 mctx.rowMerges[startRow][col] = startState endState := mctx.rowMerges[endedRow][col] endState.Vertical.End = true endState.Vertical.Span = startState.Vertical.Span mctx.rowMerges[endedRow][col] = endState ctx.logger.Debugf("Vertical merge ended at row %d, col %d, span %d", endedRow, col, startState.Vertical.Span) } delete(mergeStartRow, col) delete(mergeStartContent, col) } if currentValStr != "" { mergeState.Vertical = tw.MergeStateOption{ Present: true, Span: 1, Start: true, End: false, } mctx.rowMerges[i][col] = mergeState mergeStartRow[col] = i mergeStartContent[col] = currentValStr ctx.logger.Debugf("Vertical merge started at row %d, col %d", i, col) } else if !mergeState.Horizontal.Present { mergeState.Vertical = tw.MergeStateOption{} mctx.rowMerges[i][col] = mergeState } } } } lastRowIdx := len(ctx.rowLines) - 1 if lastRowIdx >= 0 { for col, startRow := range mergeStartRow { startState := mctx.rowMerges[startRow][col] finalSpan := (lastRowIdx - startRow) + 1 startState.Vertical.Span = finalSpan startState.Vertical.End = finalSpan == 1 mctx.rowMerges[startRow][col] = startState endState := mctx.rowMerges[lastRowIdx][col] endState.Vertical.Present = true endState.Vertical.End = true endState.Vertical.Span = finalSpan if startRow != lastRowIdx { endState.Vertical.Start = false } mctx.rowMerges[lastRowIdx][col] = endState ctx.logger.Debugf("Vertical merge finalized at row %d, col %d, span %d", lastRowIdx, col, finalSpan) } } ctx.logger.Debug("Vertical merges completed") } // buildAdjacentCells constructs cell contexts for adjacent lines. // Parameters include ctx, mctx, hctx, and direction (-1 for prev, +1 for next). // Returns a map of column indices to CellContext for the adjacent line. func (t *Table) buildAdjacentCells(ctx *renderContext, mctx *mergeContext, hctx *helperContext, direction int) map[int]tw.CellContext { adjCells := make(map[int]tw.CellContext) var adjLine []string var adjMerges map[int]tw.MergeState found := false adjPosition := hctx.position // Assume adjacent line is in the same section initially switch hctx.position { case tw.Header: targetLineIdx := hctx.lineIdx + direction if direction < 0 { // Previous if targetLineIdx >= 0 && targetLineIdx < len(ctx.headerLines) { adjLine = ctx.headerLines[targetLineIdx] adjMerges = mctx.headerMerges found = true } } else { // Next if targetLineIdx < len(ctx.headerLines) { adjLine = ctx.headerLines[targetLineIdx] adjMerges = mctx.headerMerges found = true } else if len(ctx.rowLines) > 0 && len(ctx.rowLines[0]) > 0 && len(mctx.rowMerges) > 0 { adjLine = ctx.rowLines[0][0] adjMerges = mctx.rowMerges[0] adjPosition = tw.Row found = true } else if len(ctx.footerLines) > 0 { adjLine = ctx.footerLines[0] adjMerges = mctx.footerMerges adjPosition = tw.Footer found = true } } case tw.Row: targetLineIdx := hctx.lineIdx + direction if hctx.rowIdx < 0 || hctx.rowIdx >= len(ctx.rowLines) || hctx.rowIdx >= len(mctx.rowMerges) { t.logger.Debugf("Warning: Invalid row index %d in buildAdjacentCells", hctx.rowIdx) return nil } currentRowLines := ctx.rowLines[hctx.rowIdx] currentMerges := mctx.rowMerges[hctx.rowIdx] if direction < 0 { // Previous if targetLineIdx >= 0 && targetLineIdx < len(currentRowLines) { adjLine = currentRowLines[targetLineIdx] adjMerges = currentMerges found = true } else if targetLineIdx < 0 { targetRowIdx := hctx.rowIdx - 1 if targetRowIdx >= 0 && targetRowIdx < len(ctx.rowLines) && targetRowIdx < len(mctx.rowMerges) { prevRowLines := ctx.rowLines[targetRowIdx] if len(prevRowLines) > 0 { adjLine = prevRowLines[len(prevRowLines)-1] adjMerges = mctx.rowMerges[targetRowIdx] found = true } } else if len(ctx.headerLines) > 0 { adjLine = ctx.headerLines[len(ctx.headerLines)-1] adjMerges = mctx.headerMerges adjPosition = tw.Header found = true } } } else { // Next if targetLineIdx >= 0 && targetLineIdx < len(currentRowLines) { adjLine = currentRowLines[targetLineIdx] adjMerges = currentMerges found = true } else if targetLineIdx >= len(currentRowLines) { targetRowIdx := hctx.rowIdx + 1 if targetRowIdx < len(ctx.rowLines) && targetRowIdx < len(mctx.rowMerges) && len(ctx.rowLines[targetRowIdx]) > 0 { adjLine = ctx.rowLines[targetRowIdx][0] adjMerges = mctx.rowMerges[targetRowIdx] found = true } else if len(ctx.footerLines) > 0 { adjLine = ctx.footerLines[0] adjMerges = mctx.footerMerges adjPosition = tw.Footer found = true } } } case tw.Footer: targetLineIdx := hctx.lineIdx + direction if direction < 0 { // Previous if targetLineIdx >= 0 && targetLineIdx < len(ctx.footerLines) { adjLine = ctx.footerLines[targetLineIdx] adjMerges = mctx.footerMerges found = true } else if targetLineIdx < 0 { if len(ctx.rowLines) > 0 { lastRowIdx := len(ctx.rowLines) - 1 if lastRowIdx < len(mctx.rowMerges) && len(ctx.rowLines[lastRowIdx]) > 0 { lastRowLines := ctx.rowLines[lastRowIdx] adjLine = lastRowLines[len(lastRowLines)-1] adjMerges = mctx.rowMerges[lastRowIdx] adjPosition = tw.Row found = true } } else if len(ctx.headerLines) > 0 { adjLine = ctx.headerLines[len(ctx.headerLines)-1] adjMerges = mctx.headerMerges adjPosition = tw.Header found = true } } } else { // Next if targetLineIdx >= 0 && targetLineIdx < len(ctx.footerLines) { adjLine = ctx.footerLines[targetLineIdx] adjMerges = mctx.footerMerges found = true } } } if !found { return nil } if adjMerges == nil { adjMerges = make(map[int]tw.MergeState) t.logger.Debugf("Warning: adjMerges was nil in buildAdjacentCells despite found=true") } paddedAdjLine := padLine(adjLine, ctx.numCols) for j := 0; j < ctx.numCols; j++ { mergeState := adjMerges[j] cellData := paddedAdjLine[j] finalAdjColWidth := ctx.widths[adjPosition].Get(j) adjCells[j] = tw.CellContext{ Data: cellData, Merge: mergeState, Width: finalAdjColWidth, } } return adjCells } // buildCellContexts creates CellContext objects for a given line in batch mode. // Parameters include ctx, mctx, hctx, aligns, and padding for rendering. // Returns a renderMergeResponse with current, previous, and next cell contexts. func (t *Table) buildCellContexts(ctx *renderContext, mctx *mergeContext, hctx *helperContext, aligns map[int]tw.Align, padding map[int]tw.Padding) renderMergeResponse { t.logger.Debugf("buildCellContexts: Building contexts for position=%s, rowIdx=%d, lineIdx=%d", hctx.position, hctx.rowIdx, hctx.lineIdx) var merges map[int]tw.MergeState switch hctx.position { case tw.Header: merges = mctx.headerMerges case tw.Row: if hctx.rowIdx >= 0 && hctx.rowIdx < len(mctx.rowMerges) && mctx.rowMerges[hctx.rowIdx] != nil { merges = mctx.rowMerges[hctx.rowIdx] } else { merges = make(map[int]tw.MergeState) t.logger.Warnf("buildCellContexts: Invalid row index %d or nil merges for row", hctx.rowIdx) } case tw.Footer: merges = mctx.footerMerges default: merges = make(map[int]tw.MergeState) t.logger.Warnf("buildCellContexts: Invalid position '%s'", hctx.position) } cells := t.buildCoreCellContexts(hctx.line, merges, ctx.widths[hctx.position], aligns, padding, ctx.numCols) return renderMergeResponse{ cells: cells, prevCells: t.buildAdjacentCells(ctx, mctx, hctx, -1), nextCells: t.buildAdjacentCells(ctx, mctx, hctx, +1), location: hctx.location, } } // buildCoreCellContexts constructs CellContext objects for a single line, shared between batch and streaming modes. // Parameters: // - line: The content of the current line (padded to numCols). // - merges: Merge states for the line's columns (map[int]tw.MergeState). // - widths: Column widths (tw.Mapper[int, int]). // - aligns: Column alignments (map[int]tw.Align). // - padding: Column padding settings (map[int]tw.Padding). // - numCols: Number of columns to process. // Returns a map of column indices to CellContext for the current line. func (t *Table) buildCoreCellContexts(line []string, merges map[int]tw.MergeState, widths tw.Mapper[int, int], aligns map[int]tw.Align, padding map[int]tw.Padding, numCols int) map[int]tw.CellContext { cells := make(map[int]tw.CellContext) paddedLine := padLine(line, numCols) for j := 0; j < numCols; j++ { cellData := paddedLine[j] mergeState := tw.MergeState{} if merges != nil { if state, ok := merges[j]; ok { mergeState = state } } cells[j] = tw.CellContext{ Data: cellData, Align: aligns[j], Padding: padding[j], Width: widths.Get(j), Merge: mergeState, } } t.logger.Debugf("buildCoreCellContexts: Built cell contexts for %d columns", numCols) return cells } // buildPaddingLineContents constructs a padding line for a given section, respecting column widths and horizontal merges. // It generates a []string where each element is the padding content for a column, using the specified padChar. func (t *Table) buildPaddingLineContents(padChar string, widths tw.Mapper[int, int], numCols int, merges map[int]tw.MergeState) []string { line := make([]string, numCols) padWidth := twwidth.Width(padChar) if padWidth < 1 { padWidth = 1 } for j := 0; j < numCols; j++ { mergeState := tw.MergeState{} if merges != nil { if state, ok := merges[j]; ok { mergeState = state } } if mergeState.Horizontal.Present && !mergeState.Horizontal.Start { line[j] = tw.Empty continue } colWd := widths.Get(j) repeatCount := 0 if colWd > 0 && padWidth > 0 { repeatCount = colWd / padWidth } if colWd > 0 && repeatCount < 1 { repeatCount = 1 } content := strings.Repeat(padChar, repeatCount) line[j] = content } if t.logger.Enabled() { t.logger.Debugf("Built padding line with char '%s' for %d columns", padChar, numCols) } return line } // calculateAndNormalizeWidths computes and normalizes column widths. // Parameter ctx holds rendering state with width maps. // Returns an error if width calculation fails. func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error { ctx.logger.Debugf("calculateAndNormalizeWidths: Computing and normalizing widths for %d columns. Compact: %v", ctx.numCols, t.config.Behavior.Compact.Merge.Enabled()) // Initialize width maps //t.headerWidths = tw.NewMapper[int, int]() //t.rowWidths = tw.NewMapper[int, int]() //t.footerWidths = tw.NewMapper[int, int]() // Compute content-based widths for each section for _, lines := range ctx.headerLines { t.updateWidths(lines, t.headerWidths, t.config.Header.Padding) } rowWidthCache := make([]tw.Mapper[int, int], len(ctx.rowLines)) for i, row := range ctx.rowLines { rowWidthCache[i] = tw.NewMapper[int, int]() for _, line := range row { t.updateWidths(line, rowWidthCache[i], t.config.Row.Padding) for col, width := range rowWidthCache[i] { currentMax, _ := t.rowWidths.OK(col) if width > currentMax { t.rowWidths.Set(col, width) } } } } for _, lines := range ctx.footerLines { t.updateWidths(lines, t.footerWidths, t.config.Footer.Padding) } ctx.logger.Debugf("Content-based widths: header=%v, row=%v, footer=%v", t.headerWidths, t.rowWidths, t.footerWidths) // Analyze header merges for optimization var headerMergeSpans map[int]int if t.config.Header.Formatting.MergeMode&tw.MergeHorizontal != 0 && len(ctx.headerLines) > 0 { headerMergeSpans = make(map[int]int) visitedCols := make(map[int]bool) firstHeaderLine := ctx.headerLines[0] if len(firstHeaderLine) > 0 { for i := 0; i < len(firstHeaderLine); { if visitedCols[i] { i++ continue } var currentLogicalCellContentBuilder strings.Builder for _, hLine := range ctx.headerLines { if i < len(hLine) { currentLogicalCellContentBuilder.WriteString(hLine[i]) } } currentHeaderCellContent := t.Trimmer(currentLogicalCellContentBuilder.String()) span := 1 for j := i + 1; j < len(firstHeaderLine); j++ { var nextLogicalCellContentBuilder strings.Builder for _, hLine := range ctx.headerLines { if j < len(hLine) { nextLogicalCellContentBuilder.WriteString(hLine[j]) } } nextHeaderCellContent := t.Trimmer(nextLogicalCellContentBuilder.String()) if currentHeaderCellContent == nextHeaderCellContent && currentHeaderCellContent != "" && currentHeaderCellContent != "-" { span++ } else { break } } if span > 1 { headerMergeSpans[i] = span for k := 0; k < span; k++ { visitedCols[i+k] = true } } i += span } } if len(headerMergeSpans) > 0 { ctx.logger.Debugf("Header merge spans: %v", headerMergeSpans) } } // Determine natural column widths naturalColumnWidths := tw.NewMapper[int, int]() for i := 0; i < ctx.numCols; i++ { width := 0 if colWidth, ok := t.config.Widths.PerColumn.OK(i); ok && colWidth >= 0 { width = colWidth ctx.logger.Debugf("Col %d width from Config.Widths.PerColumn: %d", i, width) } else { maxRowFooterWidth := tw.Max(t.rowWidths.Get(i), t.footerWidths.Get(i)) headerCellOriginalWidth := t.headerWidths.Get(i) if t.config.Behavior.Compact.Merge.Enabled() && t.config.Header.Formatting.MergeMode&tw.MergeHorizontal != 0 && headerMergeSpans != nil { isColInHeaderMerge := false for startCol, span := range headerMergeSpans { if i >= startCol && i < startCol+span { isColInHeaderMerge = true break } } if isColInHeaderMerge { width = maxRowFooterWidth if width == 0 && headerCellOriginalWidth > 0 { width = headerCellOriginalWidth } ctx.logger.Debugf("Col %d (in merge) width: %d (row/footer: %d, header: %d)", i, width, maxRowFooterWidth, headerCellOriginalWidth) } else { width = tw.Max(headerCellOriginalWidth, maxRowFooterWidth) ctx.logger.Debugf("Col %d (not in merge) width: %d", i, width) } } else { width = tw.Max(tw.Max(headerCellOriginalWidth, t.rowWidths.Get(i)), t.footerWidths.Get(i)) ctx.logger.Debugf("Col %d width (no merge): %d", i, width) } if width == 0 && (headerCellOriginalWidth > 0 || t.rowWidths.Get(i) > 0 || t.footerWidths.Get(i) > 0) { width = tw.Max(tw.Max(headerCellOriginalWidth, t.rowWidths.Get(i)), t.footerWidths.Get(i)) } if width == 0 { width = 1 } } naturalColumnWidths.Set(i, width) } ctx.logger.Debugf("Natural column widths: %v", naturalColumnWidths) // Expand columns for merged header content if needed workingWidths := naturalColumnWidths.Clone() if t.config.Header.Formatting.MergeMode&tw.MergeHorizontal != 0 && headerMergeSpans != nil { if span, isOneBigMerge := headerMergeSpans[0]; isOneBigMerge && span == ctx.numCols && ctx.numCols > 0 { var firstHeaderCellLogicalContentBuilder strings.Builder for _, hLine := range ctx.headerLines { if 0 < len(hLine) { firstHeaderCellLogicalContentBuilder.WriteString(hLine[0]) } } mergedContentString := t.Trimmer(firstHeaderCellLogicalContentBuilder.String()) headerCellPadding := t.config.Header.Padding.Global if 0 < len(t.config.Header.Padding.PerColumn) && t.config.Header.Padding.PerColumn[0].Paddable() { headerCellPadding = t.config.Header.Padding.PerColumn[0] } actualMergedHeaderContentPhysicalWidth := twwidth.Width(mergedContentString) + twwidth.Width(headerCellPadding.Left) + twwidth.Width(headerCellPadding.Right) currentSumOfColumnWidths := 0 workingWidths.Each(func(_ int, w int) { currentSumOfColumnWidths += w }) numSeparatorsInFullSpan := 0 if ctx.numCols > 1 { if t.renderer != nil && t.renderer.Config().Settings.Separators.BetweenColumns.Enabled() { numSeparatorsInFullSpan = (ctx.numCols - 1) * twwidth.Width(t.renderer.Config().Symbols.Column()) } } totalCurrentSpanPhysicalWidth := currentSumOfColumnWidths + numSeparatorsInFullSpan if actualMergedHeaderContentPhysicalWidth > totalCurrentSpanPhysicalWidth { ctx.logger.Debugf("Merged header content '%s' (width %d) exceeds total width %d. Expanding.", mergedContentString, actualMergedHeaderContentPhysicalWidth, totalCurrentSpanPhysicalWidth) shortfall := actualMergedHeaderContentPhysicalWidth - totalCurrentSpanPhysicalWidth numNonZeroCols := 0 workingWidths.Each(func(_ int, w int) { if w > 0 { numNonZeroCols++ } }) if numNonZeroCols == 0 && ctx.numCols > 0 { numNonZeroCols = ctx.numCols } if numNonZeroCols > 0 && shortfall > 0 { extraPerColumn := int(math.Ceil(float64(shortfall) / float64(numNonZeroCols))) finalSumAfterExpansion := 0 workingWidths.Each(func(colIdx int, currentW int) { if currentW > 0 || (numNonZeroCols == ctx.numCols && ctx.numCols > 0) { newWidth := currentW + extraPerColumn workingWidths.Set(colIdx, newWidth) finalSumAfterExpansion += newWidth ctx.logger.Debugf("Col %d expanded by %d to %d", colIdx, extraPerColumn, newWidth) } else { finalSumAfterExpansion += currentW } }) overDistributed := (finalSumAfterExpansion + numSeparatorsInFullSpan) - actualMergedHeaderContentPhysicalWidth if overDistributed > 0 { ctx.logger.Debugf("Correcting over-distribution of %d", overDistributed) // Sort columns for deterministic reduction sortedCols := workingWidths.SortedKeys() for i := 0; i < overDistributed; i++ { // Reduce from highest-indexed column for j := len(sortedCols) - 1; j >= 0; j-- { col := sortedCols[j] if workingWidths.Get(col) > 1 && naturalColumnWidths.Get(col) < workingWidths.Get(col) { workingWidths.Set(col, workingWidths.Get(col)-1) ctx.logger.Debugf("Reduced col %d by 1 to %d", col, workingWidths.Get(col)) break } } } } } } } } ctx.logger.Debugf("Widths after merged header expansion: %v", workingWidths) // Apply global width constraint finalWidths := workingWidths.Clone() if t.config.Widths.Global > 0 { ctx.logger.Debugf("Applying global width constraint: %d", t.config.Widths.Global) currentSumOfFinalColWidths := 0 finalWidths.Each(func(_ int, w int) { currentSumOfFinalColWidths += w }) numSeparators := 0 if ctx.numCols > 1 && t.renderer != nil && t.renderer.Config().Settings.Separators.BetweenColumns.Enabled() { numSeparators = (ctx.numCols - 1) * twwidth.Width(t.renderer.Config().Symbols.Column()) } totalCurrentTablePhysicalWidth := currentSumOfFinalColWidths + numSeparators if totalCurrentTablePhysicalWidth > t.config.Widths.Global { ctx.logger.Debugf("Table width %d exceeds global limit %d. Shrinking.", totalCurrentTablePhysicalWidth, t.config.Widths.Global) targetTotalColumnContentWidth := t.config.Widths.Global - numSeparators if targetTotalColumnContentWidth < 0 { targetTotalColumnContentWidth = 0 } if ctx.numCols > 0 && targetTotalColumnContentWidth < ctx.numCols { targetTotalColumnContentWidth = ctx.numCols } hardMinimums := tw.NewMapper[int, int]() sumOfHardMinimums := 0 isHeaderContentHardToWrap := !(t.config.Header.Formatting.AutoWrap == tw.WrapNormal || t.config.Header.Formatting.AutoWrap == tw.WrapBreak) for i := 0; i < ctx.numCols; i++ { minW := 1 if isHeaderContentHardToWrap && len(ctx.headerLines) > 0 { headerColNaturalWidthWithPadding := t.headerWidths.Get(i) if headerColNaturalWidthWithPadding > minW { minW = headerColNaturalWidthWithPadding } } hardMinimums.Set(i, minW) sumOfHardMinimums += minW } ctx.logger.Debugf("Hard minimums: %v (sum: %d)", hardMinimums, sumOfHardMinimums) if targetTotalColumnContentWidth < sumOfHardMinimums && sumOfHardMinimums > 0 { ctx.logger.Warnf("Target width %d below minimums %d. Scaling.", targetTotalColumnContentWidth, sumOfHardMinimums) scaleFactorMin := float64(targetTotalColumnContentWidth) / float64(sumOfHardMinimums) if scaleFactorMin < 0 { scaleFactorMin = 0 } tempSum := 0 scaledHardMinimums := tw.NewMapper[int, int]() hardMinimums.Each(func(colIdx int, currentMinW int) { scaledMinW := int(math.Round(float64(currentMinW) * scaleFactorMin)) if scaledMinW < 1 && targetTotalColumnContentWidth > 0 { scaledMinW = 1 } else if scaledMinW < 0 { scaledMinW = 0 } scaledHardMinimums.Set(colIdx, scaledMinW) tempSum += scaledMinW }) errorDiffMin := targetTotalColumnContentWidth - tempSum if errorDiffMin != 0 && scaledHardMinimums.Len() > 0 { sortedKeys := scaledHardMinimums.SortedKeys() for i := 0; i < int(math.Abs(float64(errorDiffMin))); i++ { keyToAdjust := sortedKeys[i%len(sortedKeys)] val := scaledHardMinimums.Get(keyToAdjust) adj := 1 if errorDiffMin < 0 { adj = -1 } if val+adj >= 1 || (val+adj == 0 && targetTotalColumnContentWidth == 0) { scaledHardMinimums.Set(keyToAdjust, val+adj) } else if adj > 0 { scaledHardMinimums.Set(keyToAdjust, val+adj) } } } finalWidths = scaledHardMinimums.Clone() ctx.logger.Debugf("Scaled minimums: %v", finalWidths) } else { finalWidths = hardMinimums.Clone() widthAllocatedByMinimums := sumOfHardMinimums remainingWidthToDistribute := targetTotalColumnContentWidth - widthAllocatedByMinimums ctx.logger.Debugf("Target: %d, minimums: %d, remaining: %d", targetTotalColumnContentWidth, widthAllocatedByMinimums, remainingWidthToDistribute) if remainingWidthToDistribute > 0 { sumOfFlexiblePotentialBase := 0 flexibleColsOriginalWidths := tw.NewMapper[int, int]() for i := 0; i < ctx.numCols; i++ { naturalW := workingWidths.Get(i) minW := hardMinimums.Get(i) if naturalW > minW { sumOfFlexiblePotentialBase += (naturalW - minW) flexibleColsOriginalWidths.Set(i, naturalW) } } ctx.logger.Debugf("Flexible potential: %d, flexible widths: %v", sumOfFlexiblePotentialBase, flexibleColsOriginalWidths) if sumOfFlexiblePotentialBase > 0 { distributedExtraSum := 0 sortedFlexKeys := flexibleColsOriginalWidths.SortedKeys() for _, colIdx := range sortedFlexKeys { naturalWOfCol := flexibleColsOriginalWidths.Get(colIdx) hardMinOfCol := hardMinimums.Get(colIdx) flexiblePartOfCol := naturalWOfCol - hardMinOfCol proportion := 0.0 if sumOfFlexiblePotentialBase > 0 { proportion = float64(flexiblePartOfCol) / float64(sumOfFlexiblePotentialBase) } else if len(sortedFlexKeys) > 0 { proportion = 1.0 / float64(len(sortedFlexKeys)) } extraForThisCol := int(math.Round(float64(remainingWidthToDistribute) * proportion)) currentAssignedW := finalWidths.Get(colIdx) finalWidths.Set(colIdx, currentAssignedW+extraForThisCol) distributedExtraSum += extraForThisCol } errorInDist := remainingWidthToDistribute - distributedExtraSum ctx.logger.Debugf("Distributed %d, error: %d", distributedExtraSum, errorInDist) if errorInDist != 0 && len(sortedFlexKeys) > 0 { for i := 0; i < int(math.Abs(float64(errorInDist))); i++ { colToAdjust := sortedFlexKeys[i%len(sortedFlexKeys)] w := finalWidths.Get(colToAdjust) adj := 1 if errorInDist < 0 { adj = -1 } if !(adj < 0 && w+adj < hardMinimums.Get(colToAdjust)) { finalWidths.Set(colToAdjust, w+adj) } else if adj > 0 { finalWidths.Set(colToAdjust, w+adj) } } } } else { if ctx.numCols > 0 { extraPerCol := remainingWidthToDistribute / ctx.numCols rem := remainingWidthToDistribute % ctx.numCols for i := 0; i < ctx.numCols; i++ { currentW := finalWidths.Get(i) add := extraPerCol if i < rem { add++ } finalWidths.Set(i, currentW+add) } } } } } finalSumCheck := 0 finalWidths.Each(func(idx int, w int) { if w < 1 && targetTotalColumnContentWidth > 0 { finalWidths.Set(idx, 1) } else if w < 0 { finalWidths.Set(idx, 0) } finalSumCheck += finalWidths.Get(idx) }) ctx.logger.Debugf("Final widths after scaling: %v (sum: %d, target: %d)", finalWidths, finalSumCheck, targetTotalColumnContentWidth) } } // Assign final widths to context ctx.widths[tw.Header] = finalWidths.Clone() ctx.widths[tw.Row] = finalWidths.Clone() ctx.widths[tw.Footer] = finalWidths.Clone() ctx.logger.Debugf("Final normalized widths: header=%v, row=%v, footer=%v", ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer]) return nil } // calculateContentMaxWidth computes the maximum content width for a column, accounting for padding and mode-specific constraints. // Returns the effective content width (after subtracting padding) for the given column index. func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLeftWidth, padRightWidth int, isStreaming bool) int { var effectiveContentMaxWidth int if isStreaming { // Existing streaming logic remains unchanged totalColumnWidthFromStream := t.streamWidths.Get(colIdx) if totalColumnWidthFromStream < 0 { totalColumnWidthFromStream = 0 } effectiveContentMaxWidth = totalColumnWidthFromStream - padLeftWidth - padRightWidth if effectiveContentMaxWidth < 1 && totalColumnWidthFromStream > (padLeftWidth+padRightWidth) { effectiveContentMaxWidth = 1 } else if effectiveContentMaxWidth < 0 { effectiveContentMaxWidth = 0 } if totalColumnWidthFromStream == 0 { effectiveContentMaxWidth = 0 } t.logger.Debugf("calculateContentMaxWidth: Streaming col %d, TotalColWd=%d, PadL=%d, PadR=%d -> ContentMaxWd=%d", colIdx, totalColumnWidthFromStream, padLeftWidth, padRightWidth, effectiveContentMaxWidth) } else { // New priority-based width constraint checking constraintTotalCellWidth := 0 hasConstraint := false // 1. Check new Widths.PerColumn (highest priority) if t.config.Widths.Constrained() { if colWidth, ok := t.config.Widths.PerColumn.OK(colIdx); ok && colWidth > 0 { constraintTotalCellWidth = colWidth hasConstraint = true t.logger.Debugf("calculateContentMaxWidth: Using Widths.PerColumn[%d] = %d", colIdx, constraintTotalCellWidth) } // 2. Check new Widths.Global if !hasConstraint && t.config.Widths.Global > 0 { constraintTotalCellWidth = t.config.Widths.Global hasConstraint = true t.logger.Debugf("calculateContentMaxWidth: Using Widths.Global = %d", constraintTotalCellWidth) } } // 3. Fall back to legacy ColMaxWidths.PerColumn (backward compatibility) if !hasConstraint && config.ColMaxWidths.PerColumn != nil { if colMax, ok := config.ColMaxWidths.PerColumn.OK(colIdx); ok && colMax > 0 { constraintTotalCellWidth = colMax hasConstraint = true t.logger.Debugf("calculateContentMaxWidth: Using legacy ColMaxWidths.PerColumn[%d] = %d", colIdx, constraintTotalCellWidth) } } // 4. Fall back to legacy ColMaxWidths.Global if !hasConstraint && config.ColMaxWidths.Global > 0 { constraintTotalCellWidth = config.ColMaxWidths.Global hasConstraint = true t.logger.Debugf("calculateContentMaxWidth: Using legacy ColMaxWidths.Global = %d", constraintTotalCellWidth) } // 5. Fall back to table MaxWidth if auto-wrapping if !hasConstraint && t.config.MaxWidth > 0 && config.Formatting.AutoWrap != tw.WrapNone { constraintTotalCellWidth = t.config.MaxWidth hasConstraint = true t.logger.Debugf("calculateContentMaxWidth: Using table MaxWidth = %d (AutoWrap enabled)", constraintTotalCellWidth) } // Calculate effective width based on found constraint if hasConstraint { effectiveContentMaxWidth = constraintTotalCellWidth - padLeftWidth - padRightWidth if effectiveContentMaxWidth < 1 && constraintTotalCellWidth > (padLeftWidth+padRightWidth) { effectiveContentMaxWidth = 1 } else if effectiveContentMaxWidth < 0 { effectiveContentMaxWidth = 0 } t.logger.Debugf("calculateContentMaxWidth: ConstraintTotalCellWidth=%d, PadL=%d, PadR=%d -> EffectiveContentMaxWidth=%d", constraintTotalCellWidth, padLeftWidth, padRightWidth, effectiveContentMaxWidth) } else { effectiveContentMaxWidth = 0 t.logger.Debugf("calculateContentMaxWidth: No width constraints found for column %d", colIdx) } } return effectiveContentMaxWidth } // convertToStringer invokes the table's stringer function with optional caching. func (t *Table) convertToStringer(input interface{}) ([]string, error) { // This function is now only called if t.stringer is non-nil. if t.stringer == nil { return nil, errors.New("internal error: convertToStringer called with nil t.stringer") } t.logger.Debugf("convertToString attempt %v using %v", input, t.stringer) inputType := reflect.TypeOf(input) stringerFuncVal := reflect.ValueOf(t.stringer) stringerFuncType := stringerFuncVal.Type() // Cache lookup (simplified, actual cache logic can be more complex) if t.stringerCacheEnabled { t.stringerCacheMu.RLock() cachedFunc, ok := t.stringerCache[inputType] t.stringerCacheMu.RUnlock() if ok { // Add proper type checking for cachedFunc against input here if necessary t.logger.Debugf("convertToStringer: Cache hit for type %v", inputType) results := cachedFunc.Call([]reflect.Value{reflect.ValueOf(input)}) if len(results) == 1 && results[0].Type() == reflect.TypeOf([]string{}) { return results[0].Interface().([]string), nil } } } // Robust type checking for the stringer function validSignature := stringerFuncVal.Kind() == reflect.Func && stringerFuncType.NumIn() == 1 && stringerFuncType.NumOut() == 1 && stringerFuncType.Out(0) == reflect.TypeOf([]string{}) if !validSignature { return nil, errors.Newf("table stringer (type %T) does not have signature func(SomeType) []string", t.stringer) } // Check if input is assignable to stringer's parameter type paramType := stringerFuncType.In(0) assignable := false if inputType != nil { // input is not untyped nil if inputType.AssignableTo(paramType) { assignable = true } else if paramType.Kind() == reflect.Interface && inputType.Implements(paramType) { assignable = true } else if paramType.Kind() == reflect.Interface && paramType.NumMethod() == 0 { // stringer expects interface{} assignable = true } } else if paramType.Kind() == reflect.Interface || (paramType.Kind() == reflect.Ptr && paramType.Elem().Kind() != reflect.Interface) { // If input is nil, it can be assigned if stringer expects an interface or a pointer type // (but not a pointer to an interface, which is rare for stringers). // A nil value for a concrete type parameter would cause a panic on Call. // So, if paramType is not an interface/pointer, and input is nil, it's an issue. // This needs careful handling. For now, assume assignable if interface/pointer. assignable = true } if !assignable { return nil, errors.Newf("input type %T cannot be passed to table stringer expecting %s", input, paramType) } var callArgs []reflect.Value if input == nil { // If input is nil, we must pass a zero value of the stringer's parameter type // if that type is a pointer or interface. // Passing reflect.ValueOf(nil) directly will cause issues if paramType is concrete. callArgs = []reflect.Value{reflect.Zero(paramType)} } else { callArgs = []reflect.Value{reflect.ValueOf(input)} } resultValues := stringerFuncVal.Call(callArgs) if t.stringerCacheEnabled && inputType != nil { // Only cache if inputType is valid t.stringerCacheMu.Lock() t.stringerCache[inputType] = stringerFuncVal t.stringerCacheMu.Unlock() } return resultValues[0].Interface().([]string), nil } // convertToString converts a value to its string representation. func (t *Table) convertToString(value interface{}) string { if value == nil { return "" } switch v := value.(type) { case tw.Formatter: return v.Format() case io.Reader: const maxReadSize = 512 var buf strings.Builder _, err := io.CopyN(&buf, v, maxReadSize) if err != nil && err != io.EOF { return fmt.Sprintf("[reader error: %v]", err) // Keep fmt.Sprintf for rare error case } if buf.Len() == maxReadSize { buf.WriteString(tw.CharEllipsis) } return buf.String() case sql.NullString: if v.Valid { return v.String } return "" case sql.NullInt64: if v.Valid { return strconv.FormatInt(v.Int64, 10) } return "" case sql.NullFloat64: if v.Valid { return strconv.FormatFloat(v.Float64, 'f', -1, 64) } return "" case sql.NullBool: if v.Valid { return strconv.FormatBool(v.Bool) } return "" case sql.NullTime: if v.Valid { return v.Time.String() } return "" case []byte: return string(v) case error: return v.Error() case fmt.Stringer: return v.String() case string: return v case int: return strconv.FormatInt(int64(v), 10) case int8: return strconv.FormatInt(int64(v), 10) case int16: return strconv.FormatInt(int64(v), 10) case int32: return strconv.FormatInt(int64(v), 10) case int64: return strconv.FormatInt(v, 10) case uint: return strconv.FormatUint(uint64(v), 10) case uint8: return strconv.FormatUint(uint64(v), 10) case uint16: return strconv.FormatUint(uint64(v), 10) case uint32: return strconv.FormatUint(uint64(v), 10) case uint64: return strconv.FormatUint(v, 10) case float32: return strconv.FormatFloat(float64(v), 'f', -1, 32) case float64: return strconv.FormatFloat(v, 'f', -1, 64) case bool: return strconv.FormatBool(v) default: t.logger.Debugf("convertToString: Falling back to fmt.Sprintf for type %T", value) return fmt.Sprintf("%v", value) // Fallback for rare types } } // convertItemToCells is responsible for converting a single input item (which could be // a struct, a basic type, or an item implementing Stringer/Formatter) into a slice // of strings, where each string represents a cell for the table row. // zoo.go // convertItemToCells is responsible for converting a single input item into a slice of strings. // It now uses the unified struct parser for structs. func (t *Table) convertItemToCells(item interface{}) ([]string, error) { t.logger.Debugf("convertItemToCells: Converting item of type %T", item) // 1. User-defined table-wide stringer (t.stringer) takes highest precedence. if t.stringer != nil { res, err := t.convertToStringer(item) if err == nil { t.logger.Debugf("convertItemToCells: Used custom table stringer for type %T. Produced %d cells: %v", item, len(res), res) return res, nil } t.logger.Warnf("convertItemToCells: Custom table stringer was set but incompatible for type %T: %v. Will attempt other methods.", item, err) } // 2. Handle untyped nil directly. if item == nil { t.logger.Debugf("convertItemToCells: Item is untyped nil. Returning single empty cell.") return []string{""}, nil } // 3. Use the new unified struct parser. It handles pointers and embedding. // We only care about the values it returns. _, values := t.extractFieldsAndValuesFromStruct(item) if values != nil { t.logger.Debugf("convertItemToCells: Structs %T reflected into %d cells: %v", item, len(values), values) return values, nil } // 4. Fallback for any other single item (e.g., basic types, or types that implement Stringer/Formatter). // This code path is now for non-struct types. if formatter, ok := item.(tw.Formatter); ok { t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is tw.Formatter. Using Format().", item) return []string{formatter.Format()}, nil } if stringer, ok := item.(fmt.Stringer); ok { t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is fmt.Stringer. Using String().", item) return []string{stringer.String()}, nil } t.logger.Debugf("convertItemToCells: Item (type %T) is a basic type. Treating as single cell via convertToString.", item) return []string{t.convertToString(item)}, nil } // convertCellsToStrings converts a row to its raw string representation using specified cell config for filters. // 'rowInput' can be []string, []any, or a custom type if t.stringer is set. func (t *Table) convertCellsToStrings(rowInput interface{}, cellCfg tw.CellConfig) ([]string, error) { t.logger.Debugf("convertCellsToStrings: Converting row: %v (type: %T)", rowInput, rowInput) var cells []string var err error switch v := rowInput.(type) { //Directly supported slice types case []string: cells = v case []interface{}: // Catches variadic simple types grouped by Append cells = make([]string, len(v)) for i, val := range v { cells[i] = t.convertToString(val) } case []int: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.Itoa(val) } case []int8: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.FormatInt(int64(val), 10) } case []int16: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.FormatInt(int64(val), 10) } case []int32: // Also rune cells = make([]string, len(v)) for i, val := range v { cells[i] = t.convertToString(val) } // Use convertToString for potential rune case []int64: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.FormatInt(val, 10) } case []uint: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.FormatUint(uint64(val), 10) } case []uint8: // Also byte cells = make([]string, len(v)) // If it's truly []byte, convertToString will handle it as a string. // If it's a slice of small numbers, convertToString will handle them individually. for i, val := range v { cells[i] = t.convertToString(val) } case []uint16: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.FormatUint(uint64(val), 10) } case []uint32: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.FormatUint(uint64(val), 10) } case []uint64: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.FormatUint(val, 10) } case []float32: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.FormatFloat(float64(val), 'f', -1, 32) } case []float64: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.FormatFloat(val, 'f', -1, 64) } case []bool: cells = make([]string, len(v)) for i, val := range v { cells[i] = strconv.FormatBool(val) } case []tw.Formatter: cells = make([]string, len(v)) for i, val := range v { cells[i] = val.Format() } case []fmt.Stringer: cells = make([]string, len(v)) for i, val := range v { cells[i] = val.String() } //Cases for single items that are NOT slices // These are now dispatched to convertItemToCells by the default case. // Keeping direct tw.Formatter and fmt.Stringer here could be a micro-optimization // if `rowInput` is *exactly* that type (not a struct implementing it), // but for clarity, `convertItemToCells` can handle these too. // For this iteration, to match the described flow: case tw.Formatter: // This handles a single Formatter item t.logger.Debugf("convertCellsToStrings: Input is a single tw.Formatter. Using Format().") cells = []string{v.Format()} case fmt.Stringer: // This handles a single Stringer item t.logger.Debugf("convertCellsToStrings: Input is a single fmt.Stringer. Using String().") cells = []string{v.String()} default: // If rowInput is not one of the recognized slice types above, // or not a single Formatter/Stringer that was directly matched, // it's treated as a single item that needs to be converted into potentially multiple cells. // This is where structs (for field expansion) or other single values (for a single cell) are handled. t.logger.Debugf("convertCellsToStrings: Default case for type %T. Dispatching to convertItemToCells.", rowInput) cells, err = t.convertItemToCells(rowInput) if err != nil { t.logger.Errorf("convertCellsToStrings: Error from convertItemToCells for type %T: %v", rowInput, err) return nil, err } } // Apply filters (common logic for all successful conversions) if err == nil && cells != nil { if cellCfg.Filter.Global != nil { t.logger.Debugf("convertCellsToStrings: Applying global filter to cells: %v", cells) cells = cellCfg.Filter.Global(cells) } if len(cellCfg.Filter.PerColumn) > 0 { t.logger.Debugf("convertCellsToStrings: Applying per-column filters to %d cells", len(cells)) for i := 0; i < len(cellCfg.Filter.PerColumn); i++ { if i < len(cells) && cellCfg.Filter.PerColumn[i] != nil { originalCell := cells[i] cells[i] = cellCfg.Filter.PerColumn[i](cells[i]) if cells[i] != originalCell { t.logger.Debugf(" convertCellsToStrings: Col %d filter applied: '%s' -> '%s'", i, originalCell, cells[i]) } } else if i >= len(cells) && cellCfg.Filter.PerColumn[i] != nil { t.logger.Warnf(" convertCellsToStrings: Per-column filter defined for col %d, but item only produced %d cells. Filter for this column skipped.", i, len(cells)) } } } } if err != nil { t.logger.Debugf("convertCellsToStrings: Returning with error: %v", err) return nil, err } t.logger.Debugf("convertCellsToStrings: Conversion and filtering completed, raw cells: %v", cells) return cells, nil } // determineLocation determines the boundary location for a line. // Parameters include lineIdx, totalLines, topPad, and bottomPad. // Returns a tw.Location indicating First, Middle, or End. func (t *Table) determineLocation(lineIdx, totalLines int, topPad, bottomPad string) tw.Location { if lineIdx == 0 && topPad == tw.Empty { return tw.LocationFirst } if lineIdx == totalLines-1 && bottomPad == tw.Empty { return tw.LocationEnd } return tw.LocationMiddle } // ensureStreamWidthsCalculated ensures that stream widths and column count are initialized for streaming mode. // It uses sampleData and sectionConfig to calculate widths if not already set. // Returns an error if the column count cannot be determined. func (t *Table) ensureStreamWidthsCalculated(sampleData []string, sectionConfig tw.CellConfig) error { if t.streamWidths != nil && t.streamWidths.Len() > 0 { t.logger.Debugf("Stream widths already set: %v", t.streamWidths) return nil } t.streamCalculateWidths(sampleData, sectionConfig) if t.streamNumCols == 0 { t.logger.Warn("Failed to determine column count from sample data") return errors.New("failed to determine column count for streaming") } for i := 0; i < t.streamNumCols; i++ { if _, ok := t.streamWidths.OK(i); !ok { t.streamWidths.Set(i, 0) } } t.logger.Debugf("Initialized stream widths: %v", t.streamWidths) return nil } // getColMaxWidths retrieves maximum column widths for a section. // Parameter position specifies the section (Header, Row, Footer). // Returns a map of column indices to maximum widths. func (t *Table) getColMaxWidths(position tw.Position) tw.CellWidth { switch position { case tw.Header: return t.config.Header.ColMaxWidths case tw.Row: return t.config.Row.ColMaxWidths case tw.Footer: return t.config.Footer.ColMaxWidths default: return tw.CellWidth{} } } // getEmptyColumnInfo identifies empty columns in row data. // Parameter numOriginalCols specifies the total column count. // Returns a boolean slice (true for empty) and visible column count. func (t *Table) getEmptyColumnInfo(numOriginalCols int) (isEmpty []bool, visibleColCount int) { isEmpty = make([]bool, numOriginalCols) for i := range isEmpty { isEmpty[i] = true } if t.config.Behavior.AutoHide.Disabled() { t.logger.Debugf("getEmptyColumnInfo: AutoHide disabled, marking all %d columns as visible.", numOriginalCols) for i := range isEmpty { isEmpty[i] = false } visibleColCount = numOriginalCols return isEmpty, visibleColCount } t.logger.Debugf("getEmptyColumnInfo: Checking %d rows for %d columns...", len(t.rows), numOriginalCols) for rowIdx, logicalRow := range t.rows { for lineIdx, visualLine := range logicalRow { for colIdx, cellContent := range visualLine { if colIdx >= numOriginalCols { continue } if !isEmpty[colIdx] { continue } cellContent = t.Trimmer(cellContent) if cellContent != "" { isEmpty[colIdx] = false t.logger.Debugf("getEmptyColumnInfo: Found content in row %d, line %d, col %d ('%s'). Marked as not empty.", rowIdx, lineIdx, colIdx, cellContent) } } } } visibleColCount = 0 for _, empty := range isEmpty { if !empty { visibleColCount++ } } t.logger.Debugf("getEmptyColumnInfo: Detection complete. isEmpty: %v, visibleColCount: %d", isEmpty, visibleColCount) return isEmpty, visibleColCount } // getNumColsToUse determines the number of columns to use for rendering, based on streaming or batch mode. // Returns the number of columns (streamNumCols for streaming, maxColumns for batch). func (t *Table) getNumColsToUse() int { if t.config.Stream.Enable && t.hasPrinted { t.logger.Debugf("getNumColsToUse: Using streamNumCols: %d", t.streamNumCols) return t.streamNumCols } // For batch mode: if t.isBatchRenderNumColsSet { // If the flag is set, batchRenderNumCols holds the authoritative count // for the current Render() pass, even if that count is 0. t.logger.Debugf("getNumColsToUse (batch): Using cached t.batchRenderNumCols: %d (because isBatchRenderNumColsSet is true)", t.batchRenderNumCols) return t.batchRenderNumCols } // Fallback: If not streaming and cache flag is not set (e.g., called outside a Render pass) num := t.maxColumns() t.logger.Debugf("getNumColsToUse (batch): Cache not active, calculated via t.maxColumns(): %d", num) return num } // prepareTableSection prepares either headers or footers for the table func (t *Table) prepareTableSection(elements []any, config tw.CellConfig, sectionName string) [][]string { actualCellsToProcess := t.processVariadic(elements) t.logger.Debugf("%s(): Effective cells to process: %v", sectionName, actualCellsToProcess) stringsResult, err := t.convertCellsToStrings(actualCellsToProcess, config) if err != nil { t.logger.Errorf("%s(): Failed to convert elements to strings: %v", sectionName, err) stringsResult = []string{} } prepared := t.prepareContent(stringsResult, config) numColsBatch := t.maxColumns() if len(prepared) > 0 { for i := range prepared { if len(prepared[i]) < numColsBatch { t.logger.Debugf("Padding %s line %d from %d to %d columns", sectionName, i, len(prepared[i]), numColsBatch) paddedLine := make([]string, numColsBatch) copy(paddedLine, prepared[i]) for j := len(prepared[i]); j < numColsBatch; j++ { paddedLine[j] = tw.Empty } prepared[i] = paddedLine } else if len(prepared[i]) > numColsBatch { t.logger.Debugf("Truncating %s line %d from %d to %d columns", sectionName, i, len(prepared[i]), numColsBatch) prepared[i] = prepared[i][:numColsBatch] } } } return prepared } // processVariadic handles the common logic for processing variadic arguments // that could be either individual elements or a slice of elements func (t *Table) processVariadic(elements []any) []any { if len(elements) == 1 { switch v := elements[0].(type) { case []string: t.logger.Debugf("Detected single []string argument. Unpacking it (fast path).") out := make([]any, len(v)) for i := range v { out[i] = v[i] } return out case []interface{}: t.logger.Debugf("Detected single []interface{} argument. Unpacking it (fast path).") out := make([]any, len(v)) copy(out, v) return out } } t.logger.Debugf("Input has multiple elements or single non-slice. Using variadic elements as-is.") return elements } // toStringLines converts raw cells to formatted lines for table output func (t *Table) toStringLines(row interface{}, config tw.CellConfig) ([][]string, error) { cells, err := t.convertCellsToStrings(row, config) if err != nil { return nil, err } return t.prepareContent(cells, config), nil } // updateWidths updates the width map based on cell content and padding. // Parameters include row content, widths map, and padding configuration. // No return value. func (t *Table) updateWidths(row []string, widths tw.Mapper[int, int], padding tw.CellPadding) { t.logger.Debugf("Updating widths for row: %v", row) for i, cell := range row { colPad := padding.Global if i < len(padding.PerColumn) && padding.PerColumn[i].Paddable() { colPad = padding.PerColumn[i] t.logger.Debugf(" Col %d: Using per-column padding: L:'%s' R:'%s'", i, colPad.Left, colPad.Right) } else { t.logger.Debugf(" Col %d: Using global padding: L:'%s' R:'%s'", i, padding.Global.Left, padding.Global.Right) } padLeftWidth := twwidth.Width(colPad.Left) padRightWidth := twwidth.Width(colPad.Right) // Split cell into lines and find maximum content width lines := strings.Split(cell, tw.NewLine) contentWidth := 0 for _, line := range lines { lineWidth := twwidth.Width(line) if t.config.Behavior.TrimSpace.Enabled() { lineWidth = twwidth.Width(t.Trimmer(line)) } if lineWidth > contentWidth { contentWidth = lineWidth } } totalWidth := contentWidth + padLeftWidth + padRightWidth minRequiredPaddingWidth := padLeftWidth + padRightWidth if contentWidth == 0 && totalWidth < minRequiredPaddingWidth { t.logger.Debugf(" Col %d: Empty content, ensuring width >= padding width (%d). Setting totalWidth to %d.", i, minRequiredPaddingWidth, minRequiredPaddingWidth) totalWidth = minRequiredPaddingWidth } if totalWidth < 1 { t.logger.Debugf(" Col %d: Calculated totalWidth is zero, setting minimum width to 1.", i) totalWidth = 1 } currentMax, _ := widths.OK(i) if totalWidth > currentMax { widths.Set(i, totalWidth) t.logger.Debugf(" Col %d: Updated width from %d to %d (content:%d + padL:%d + padR:%d) for cell '%s'", i, currentMax, totalWidth, contentWidth, padLeftWidth, padRightWidth, cell) } else { t.logger.Debugf(" Col %d: Width %d not greater than current max %d for cell '%s'", i, totalWidth, currentMax, cell) } } } // extractHeadersFromStruct is now a thin wrapper around the new unified function. // It only cares about the header names. func (t *Table) extractHeadersFromStruct(sample interface{}) []string { headers, _ := t.extractFieldsAndValuesFromStruct(sample) return headers } // extractFieldsAndValuesFromStruct is the new single source of truth for struct reflection. // It recursively processes a struct, handling pointers and embedded structs, // and returns two slices: one for header names and one for string-converted values. func (t *Table) extractFieldsAndValuesFromStruct(sample interface{}) ([]string, []string) { v := reflect.ValueOf(sample) if v.Kind() == reflect.Ptr { if v.IsNil() { return nil, nil } v = v.Elem() } if v.Kind() != reflect.Struct { return nil, nil } typ := v.Type() headers := make([]string, 0, typ.NumField()) values := make([]string, 0, typ.NumField()) for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) fieldValue := v.Field(i) // Skip unexported fields if field.PkgPath != "" { continue } // Handle embedded structs recursively if field.Anonymous { h, val := t.extractFieldsAndValuesFromStruct(fieldValue.Interface()) if h != nil { headers = append(headers, h...) values = append(values, val...) } continue } var tagName string skipField := false // Loop through the priority list of configured tags (e.g., ["json", "db"]) for _, tagKey := range t.config.Behavior.Structs.Tags { tagValue := field.Tag.Get(tagKey) // If a tag is found... if tagValue != "" { // If the tag is "-", this field should be skipped entirely. if tagValue == "-" { skipField = true break // Stop processing tags for this field. } // Otherwise, we've found our highest-priority tag. Store it and stop. tagName = tagValue break // Stop processing tags for this field. } } // If the field was marked for skipping, continue to the next field. if skipField { continue } // Determine header name from the tag or fallback to the field name headerName := field.Name if tagName != "" { headerName = strings.Split(tagName, ",")[0] } headers = append(headers, tw.Title(headerName)) // Determine value, respecting omitempty from the found tag value := "" if !strings.Contains(tagName, ",omitempty") || !fieldValue.IsZero() { value = t.convertToString(fieldValue.Interface()) } values = append(values, value) } return headers, values }