pax_global_header00006660000000000000000000000064151517664430014526gustar00rootroot0000000000000052 comment=a0dea8a90a8a0c7610afb5588d2f15a57f4aa9a2 tablewriter-1.1.4/000077500000000000000000000000001515176644300140555ustar00rootroot00000000000000tablewriter-1.1.4/.github/000077500000000000000000000000001515176644300154155ustar00rootroot00000000000000tablewriter-1.1.4/.github/workflows/000077500000000000000000000000001515176644300174525ustar00rootroot00000000000000tablewriter-1.1.4/.github/workflows/go.yml000066400000000000000000000010171515176644300206010ustar00rootroot00000000000000# 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.1.4/.github/workflows/release.yaml000066400000000000000000000024221515176644300217560ustar00rootroot00000000000000name: Release on: push: tags: - 'v*' permissions: contents: write jobs: release: 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 binaries run: | # Windows GOOS=windows GOARCH=amd64 go build -o csv2table-windows-amd64.exe ./cmd/csv2table GOOS=windows GOARCH=arm64 go build -o csv2table-windows-arm64.exe ./cmd/csv2table # Linux GOOS=linux GOARCH=amd64 go build -o csv2table-linux-amd64 ./cmd/csv2table GOOS=linux GOARCH=arm64 go build -o csv2table-linux-arm64 ./cmd/csv2table # macOS GOOS=darwin GOARCH=amd64 go build -o csv2table-darwin-amd64 ./cmd/csv2table GOOS=darwin GOARCH=arm64 go build -o csv2table-darwin-arm64 ./cmd/csv2table - name: Create Release uses: softprops/action-gh-release@v1 with: files: | csv2table-windows-amd64.exe csv2table-windows-arm64.exe csv2table-linux-amd64 csv2table-linux-arm64 csv2table-darwin-amd64 csv2table-darwin-arm64 draft: false prerelease: false tablewriter-1.1.4/.gitignore000066400000000000000000000001041515176644300160400ustar00rootroot00000000000000 # folders .idea .vscode /tmp /lab dev.sh *csv2table _test/ *.test tablewriter-1.1.4/.golangci.yml000066400000000000000000000032201515176644300164360ustar00rootroot00000000000000# See for configurations: https://golangci-lint.run/usage/configuration/ version: 2 # See: https://golangci-lint.run/usage/formatters/ formatters: default: none enable: - gofmt # https://pkg.go.dev/cmd/gofmt - gofumpt # https://github.com/mvdan/gofumpt settings: gofmt: simplify: true # Simplify code: gofmt with `-s` option. gofumpt: # Module path which contains the source code being formatted. # Default: "" module-path: github.com/olekukonko/tablewriter # Should match with module in go.mod # Choose whether to use the extra rules. # Default: false extra-rules: true # See: https://golangci-lint.run/usage/linters/ linters: default: none enable: - staticcheck - govet - gocritic # - unused # TODO: There are many unused functions, should I directly remove those ? - ineffassign - unconvert - mirror - usestdlibvars - loggercheck - exptostd - godot - perfsprint # See: https://golangci-lint.run/usage/false-positives/ exclusion: # paths: # rules: settings: staticcheck: checks: - all - "-SA1019" # disabled because it warns about deprecated: kept for compatibility will be removed soon - "-ST1019" # disabled because it warns about deprecated: kept for compatibility will be removed soon - "-ST1021" # disabled because it warns to have comment on exported packages - "-ST1000" # disabled because it warns to have comment on exported functions - "-ST1020" # disabled because it warns to have at least one file in a package should have a package comment godot: period: false tablewriter-1.1.4/LICENSE.md000066400000000000000000000020421515176644300154570ustar00rootroot00000000000000Copyright (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.1.4/MIGRATION.md000066400000000000000000004603061515176644300157410ustar00rootroot00000000000000# 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 | `Merging.Mode` | `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 | `Merging.Mode` | `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").Disable() 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{Merging: tw.CellMerging{Mode: 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{Merging: tw.CellMerging{Mode: 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{Merging: tw.CellMerging{Mode: 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.1.4/README.md000066400000000000000000001037661515176644300153510ustar00rootroot00000000000000# 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.1.3 ``` **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.1.3", "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.1.3 │ 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{ Merging: tw.CellMerging{Mode: 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{ Merging: tw.CellMerging{Mode: 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{ Merging: tw.CellMerging{Mode: tw.MergeHorizontal}, // Merge identical header cells Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }, Row: tw.CellConfig{ Merging: tw.CellMerging{Mode: 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) ## What is new - `Counting` changes See [#294](https://github.com/olekukonko/tablewriter/issues/294) ## 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.1.4/README_LEGACY.md000066400000000000000000000363101515176644300163630ustar00rootroot00000000000000ASCII 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.1.4/_example/000077500000000000000000000000001515176644300156475ustar00rootroot00000000000000tablewriter-1.1.4/_example/filetable/000077500000000000000000000000001515176644300175765ustar00rootroot00000000000000tablewriter-1.1.4/_example/filetable/main.go000066400000000000000000000043001515176644300210460ustar00rootroot00000000000000package main import ( "fmt" "io/fs" "os" "path/filepath" "strings" "time" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/tw" ) 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.1.4/_example/filetable/out.txt000066400000000000000000000023441515176644300211510ustar00rootroot00000000000000┌──────────────────┬─────────┬─────────────┬──────────────┐ │ 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.1.4/_example/symbols/000077500000000000000000000000001515176644300173375ustar00rootroot00000000000000tablewriter-1.1.4/_example/symbols/main.go000066400000000000000000000111201515176644300206050ustar00rootroot00000000000000package main import ( "fmt" "os" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) 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{ Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }, Row: tw.CellConfig{ Merging: tw.CellMerging{Mode: tw.MergeHierarchical}, Alignment: tw.CellAlignment{Global: 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.1.4/_example/symbols/out.txt000066400000000000000000000213311515176644300207070ustar00rootroot00000000000000INFO: 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.1.4/_readme/000077500000000000000000000000001515176644300154515ustar00rootroot00000000000000tablewriter-1.1.4/_readme/color_1.png000066400000000000000000005140041515176644300175210ustar00rootroot00000000000000PNG  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.1.4/cmd/000077500000000000000000000000001515176644300146205ustar00rootroot00000000000000tablewriter-1.1.4/cmd/csv2table/000077500000000000000000000000001515176644300165055ustar00rootroot00000000000000tablewriter-1.1.4/cmd/csv2table/README.md000066400000000000000000000014211515176644300177620ustar00rootroot00000000000000ASCII Table Writer Tool ========= Generate ASCII table on the fly via command line ... Installation is simple as #### Install Tool go install github.com/olekukonko/tablewriter/cmd/csv2table@latest #### 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.1.4/cmd/csv2table/_data/000077500000000000000000000000001515176644300175555ustar00rootroot00000000000000tablewriter-1.1.4/cmd/csv2table/_data/test.csv000066400000000000000000000001231515176644300212450ustar00rootroot00000000000000first_name,last_name,ssn John,Barry,123456 Kathy,Smith,687987 Bob,McCornick,3979870tablewriter-1.1.4/cmd/csv2table/_data/test_info.csv000066400000000000000000000002211515176644300222570ustar00rootroot00000000000000Field,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.1.4/cmd/csv2table/csv2table.go000066400000000000000000000452221515176644300207260ustar00rootroot00000000000000package main import ( "encoding/csv" "flag" "fmt" "io" "math" "os" "strings" "unicode/utf8" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/ts" // For terminal size "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, 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, 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.1.4/comb.hcl000066400000000000000000000004621515176644300154670ustar00rootroot00000000000000recursive = true output_file = "all.txt" extensions = [".go"] exclude_dirs = [ "_examples", "_readme", "_lab","_tmp","pkg","lab","cmd","test.txt","tmp", "_readme","pkg","renderer" ] exclude_files = ["README.md","README_LEGACY.md","MIGRATION.md","test.hcl","csv.go"] use_gitignore = true detailed = truetablewriter-1.1.4/config.go000066400000000000000000000766411515176644300156670ustar00rootroot00000000000000package 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 Counter tw.Counter } // 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 } // WithTrimTab enables or disables automatic trimming of leading/trailing tabs. // Useful for preserving indentation in code blocks while trimming other whitespace. func (b *ConfigBuilder) WithTrimTab(state tw.State) *ConfigBuilder { b.config.Behavior.TrimTab = 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 } // Deprecated: Use .Footer().CellMerging().WithMode(...) instead. This method will be removed in a future version. func (b *ConfigBuilder) WithFooterMergeMode(mergeMode int) *ConfigBuilder { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return b } b.config.Footer.Merging.Mode = mergeMode 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 } // Deprecated: Use .Header().CellMerging().WithMode(...) instead. This method will be removed in a future version. func (b *ConfigBuilder) WithHeaderMergeMode(mergeMode int) *ConfigBuilder { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return b } b.config.Header.Merging.Mode = mergeMode 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 { b.config.MaxWidth = max(width, 0) 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 } // Deprecated: Use .Row().CellMerging().WithMode(...) instead. This method will be removed in a future version. func (b *ConfigBuilder) WithRowMergeMode(mergeMode int) *ConfigBuilder { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return b } b.config.Row.Merging.Mode = mergeMode 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", } } // Merging returns a HeaderMergingBuilder for configuring cell merging. func (h *HeaderConfigBuilder) Merging() *HeaderMergingBuilder { return &HeaderMergingBuilder{ parent: h, config: &h.config.Merging, } } // 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", } } // Merging returns a RowMergingBuilder for configuring cell merging. func (r *RowConfigBuilder) Merging() *RowMergingBuilder { return &RowMergingBuilder{ parent: r, config: &r.config.Merging, } } // 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", } } // Merging returns a FooterMergingBuilder for configuring cell merging. func (f *FooterConfigBuilder) Merging() *FooterMergingBuilder { return &FooterMergingBuilder{ parent: f, config: &f.config.Merging, } } // 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 } // Deprecated: Use .CellMerging().WithMode(...) instead. This method will be removed in a future version. func (hf *HeaderFormattingBuilder) WithMergeMode(mergeMode int) *HeaderFormattingBuilder { if mergeMode >= tw.MergeNone && mergeMode <= tw.MergeHierarchical { hf.parent.config.Merging.Mode = mergeMode 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 } // Deprecated: Use .CellMerging().WithMode(...) instead. This method will be removed in a future version. func (rf *RowFormattingBuilder) WithMergeMode(mergeMode int) *RowFormattingBuilder { if mergeMode >= tw.MergeNone && mergeMode <= tw.MergeHierarchical { rf.parent.config.Merging.Mode = mergeMode 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 } // Deprecated: Use .CellMerging().WithMode(...) instead. This method will be removed in a future version. func (ff *FooterFormattingBuilder) WithMergeMode(mergeMode int) *FooterFormattingBuilder { if mergeMode >= tw.MergeNone && mergeMode <= tw.MergeHierarchical { ff.parent.config.Merging.Mode = mergeMode ff.config.MergeMode = mergeMode } return ff } // HeaderMergingBuilder configures header cell merging type HeaderMergingBuilder struct { parent *HeaderConfigBuilder config *tw.CellMerging } // Build returns the parent HeaderConfigBuilder. func (hm *HeaderMergingBuilder) Build() *HeaderConfigBuilder { return hm.parent } // WithMode sets the merge mode (e.g., tw.MergeHorizontal). func (hm *HeaderMergingBuilder) WithMode(mode int) *HeaderMergingBuilder { hm.config.Mode = mode // Also set the deprecated field for backward compatibility hm.parent.config.Formatting.MergeMode = mode return hm } // ByColumnIndex sets specific columns to be merged by their index. // If not called, merging applies to all columns. func (hm *HeaderMergingBuilder) ByColumnIndex(indices []int) *HeaderMergingBuilder { if len(indices) == 0 { hm.config.ByColumnIndex = nil // nil means apply to all } else { mapper := tw.NewMapper[int, bool]() for _, idx := range indices { mapper.Set(idx, true) } hm.config.ByColumnIndex = mapper } return hm } // RowMergingBuilder configures row cell merging type RowMergingBuilder struct { parent *RowConfigBuilder config *tw.CellMerging } // Build returns the parent RowConfigBuilder. func (rm *RowMergingBuilder) Build() *RowConfigBuilder { return rm.parent } // WithMode sets the merge mode (e.g., tw.MergeVertical, tw.MergeHierarchical). func (rm *RowMergingBuilder) WithMode(mode int) *RowMergingBuilder { rm.config.Mode = mode // Also set the deprecated field for backward compatibility rm.parent.config.Formatting.MergeMode = mode return rm } // ByColumnIndex sets specific columns to be merged by their index. // If not called, merging applies to all columns. func (rm *RowMergingBuilder) ByColumnIndex(indices []int) *RowMergingBuilder { if len(indices) == 0 { rm.config.ByColumnIndex = nil // nil means apply to all } else { mapper := tw.NewMapper[int, bool]() for _, idx := range indices { mapper.Set(idx, true) } rm.config.ByColumnIndex = mapper } return rm } // FooterMergingBuilder configures footer cell merging type FooterMergingBuilder struct { parent *FooterConfigBuilder config *tw.CellMerging } // Build returns the parent FooterConfigBuilder. func (fm *FooterMergingBuilder) Build() *FooterConfigBuilder { return fm.parent } // WithMode sets the merge mode (e.g., tw.MergeHorizontal). func (fm *FooterMergingBuilder) WithMode(mode int) *FooterMergingBuilder { fm.config.Mode = mode // Also set the deprecated field for backward compatibility fm.parent.config.Formatting.MergeMode = mode return fm } // ByColumnIndex sets specific columns to be merged by their index. // If not called, merging applies to all columns. func (fm *FooterMergingBuilder) ByColumnIndex(indices []int) *FooterMergingBuilder { if len(indices) == 0 { fm.config.ByColumnIndex = nil // nil means apply to all } else { mapper := tw.NewMapper[int, bool]() for _, idx := range indices { mapper.Set(idx, true) } fm.config.ByColumnIndex = mapper } return fm } // 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 } // WithTrimTab enables/disables trim tab func (bb *BehaviorConfigBuilder) WithTrimTab(state tw.State) *BehaviorConfigBuilder { bb.config.TrimTab = 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.1.4/csv.go000066400000000000000000000063561515176644300152110ustar00rootroot00000000000000package 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.1.4/deprecated.go000066400000000000000000000213201515176644300165020ustar00rootroot00000000000000package tablewriter import ( "github.com/mattn/go-runewidth" "github.com/olekukonko/tablewriter/pkg/twwidth" "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) } } } // Deprecated: use WithEastAsian instead. // 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(cond *runewidth.Condition) Option { return func(target *Table) { twwidth.SetCondition(cond) } } tablewriter-1.1.4/go.mod000066400000000000000000000011611515176644300151620ustar00rootroot00000000000000module github.com/olekukonko/tablewriter go 1.21 require ( github.com/clipperhouse/displaywidth v0.10.0 github.com/clipperhouse/uax29/v2 v2.6.0 github.com/fatih/color v1.18.0 github.com/mattn/go-runewidth v0.0.19 github.com/olekukonko/errors v1.2.0 github.com/olekukonko/ll v0.1.6 github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 ) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect golang.org/x/sys v0.30.0 // indirect ) tablewriter-1.1.4/go.sum000066400000000000000000000043351515176644300152150ustar00rootroot00000000000000github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= 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= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= tablewriter-1.1.4/option.go000066400000000000000000000761311515176644300157240ustar00rootroot00000000000000package tablewriter import ( "reflect" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/pkg/twcache" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" ) // 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) } } } // Deprecated: Use a ConfigBuilder with .Footer().CellMerging().WithMode(...) instead. // This option will be removed in a future version. func WithFooterMergeMode(mergeMode int) Option { return func(target *Table) { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return } target.config.Footer.Merging.Mode = mergeMode 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) } } } // Deprecated: Use a ConfigBuilder with .Header().CellMerging().WithMode(...) instead. // This option will be removed in a future version. func WithHeaderMergeMode(mergeMode int) Option { return func(target *Table) { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return } target.config.Header.Merging.Mode = mergeMode 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) } } } // Deprecated: Use a ConfigBuilder with .Row().CellMerging().WithMode(...) instead. // This option will be removed in a future version. func WithRowMergeMode(mergeMode int) Option { return func(target *Table) { if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical { return } target.config.Row.Merging.Mode = mergeMode 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.stringerCache = twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity) if t.logger != nil { t.logger.Debug("Stringer updated, cache cleared") } } } // WithStringerCache enables the default LRU caching for the stringer function. // It initializes the cache with a default capacity if one does not already exist. func WithStringerCache() Option { return func(t *Table) { // Initialize default cache if strictly necessary (nil), // or if you want to ensure the default implementation is used. if t.stringerCache == nil { // NewLRU returns (Instance, error). We ignore the error here assuming capacity > 0. cache := twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity) t.stringerCache = cache } if t.logger != nil { t.logger.Debug("Option: WithStringerCache enabled (Default LRU)") } } } // WithStringerCacheCustom enables caching for the stringer function using a specific implementation. // Passing nil disables caching entirely. func WithStringerCacheCustom(cache twcache.Cache[reflect.Type, reflect.Value]) Option { return func(t *Table) { if cache == nil { t.stringerCache = nil if t.logger != nil { t.logger.Debug("Option: WithStringerCacheCustom called with nil (Caching Disabled)") } return } // Set the custom cache and enable the flag t.stringerCache = cache if t.logger != nil { t.logger.Debug("Option: WithStringerCacheCustom 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) } } } // WithTrimTab sets whether leading and trailing tab characters are automatically trimmed. // Logs the change if debugging is enabled. func WithTrimTab(state tw.State) Option { return func(target *Table) { target.config.Behavior.TrimTab = state if target.logger != nil { target.logger.Debugf("Option: WithTrimTab applied to Table: %v", state) } } } // WithTrimLine sets whether empty visual lines within a cell are trimmed. // Logs the change if debugging is enabled. func WithTrimLine(state tw.State) Option { return func(target *Table) { target.config.Behavior.TrimLine = state if target.logger != nil { target.logger.Debugf("Option: WithTrimLine 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. // - state=tw.On: Enables East Asian width calculations. CJK and ambiguous characters // are typically measured as double width. // - state=tw.Off: 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(state tw.State) Option { return func(target *Table) { if state.Enabled() { twwidth.SetEastAsian(true) } if state.Disabled() { twwidth.SetEastAsian(false) } } } // 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) } } } } // WithCounters enables line counting by wrapping the table's writer. // If a custom counter (that implements tw.Counter) is provided, it will be used. // If the provided counter is nil, a default tw.LineCounter will be used. // The final count can be retrieved via the table.Lines() method after Render() is called. func WithCounters(counters ...tw.Counter) Option { return func(target *Table) { // Iterate through the provided counters and add any non-nil ones. for _, c := range counters { if c != nil { target.counters = append(target.counters, c) } } } } // WithLineCounter enables the default line counter. // A new instance of tw.LineCounter is added to the table's list of counters. // The total count can be retrieved via the table.Lines() method after Render() is called. func WithLineCounter() Option { return func(target *Table) { // Important: Create a new instance so tables don't share counters. target.counters = append(target.counters, &tw.LineCounter{}) } } // 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, }, Merging: tw.CellMerging{ Mode: 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, }, Merging: tw.CellMerging{ Mode: 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, }, Merging: tw.CellMerging{ Mode: 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, TrimTab: tw.On, TrimLine: 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 } // Handle merging of the new CellMerging struct and the deprecated MergeMode if src.Merging.Mode != 0 { dst.Merging.Mode = src.Merging.Mode dst.Formatting.MergeMode = src.Merging.Mode } else if src.Formatting.MergeMode != 0 { dst.Merging.Mode = src.Formatting.MergeMode dst.Formatting.MergeMode = src.Formatting.MergeMode } if src.Merging.ByColumnIndex != nil { dst.Merging.ByColumnIndex = src.Merging.ByColumnIndex.Clone() } 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.TrimTab = src.Behavior.TrimTab 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.1.4/pkg/000077500000000000000000000000001515176644300146365ustar00rootroot00000000000000tablewriter-1.1.4/pkg/twcache/000077500000000000000000000000001515176644300162545ustar00rootroot00000000000000tablewriter-1.1.4/pkg/twcache/cache.go000066400000000000000000000010221515176644300176410ustar00rootroot00000000000000package twcache // Cache defines a generic interface for a key-value storage with type constraints on keys and values. // The keys must be of a type that supports comparison. // Add inserts a new key-value pair, potentially evicting an item if necessary. // Get retrieves a value associated with the given key, returning a boolean to indicate if the key was found. // Purge clears all items from the cache. type Cache[K comparable, V any] interface { Add(key K, value V) (evicted bool) Get(key K) (value V, ok bool) Purge() } tablewriter-1.1.4/pkg/twcache/lru.go000066400000000000000000000136421515176644300174130ustar00rootroot00000000000000package twcache import ( "sync" "sync/atomic" ) // EvictCallback is a function called when an entry is evicted. // This includes evictions during Purge or Resize operations. type EvictCallback[K comparable, V any] func(key K, value V) // LRU is a thread-safe, generic LRU cache with a fixed size. // It has zero dependencies, high performance, and full features. type LRU[K comparable, V any] struct { size int items map[K]*entry[K, V] head *entry[K, V] // Most Recently Used tail *entry[K, V] // Least Recently Used onEvict EvictCallback[K, V] mu sync.Mutex hits atomic.Int64 misses atomic.Int64 } // entry represents a single item in the LRU linked list. // It holds the key, value, and pointers to prev/next entries. type entry[K comparable, V any] struct { key K value V prev *entry[K, V] next *entry[K, V] } // NewLRU creates a new LRU cache with the given size. // Returns nil if size <= 0, acting as a disabled cache. // Caps size at 100,000 for reasonableness. func NewLRU[K comparable, V any](size int) *LRU[K, V] { return NewLRUEvict[K, V](size, nil) } // NewLRUEvict creates a new LRU cache with an eviction callback. // The callback is optional and called on evictions. // Returns nil if size <= 0. func NewLRUEvict[K comparable, V any](size int, onEvict EvictCallback[K, V]) *LRU[K, V] { if size <= 0 { return nil // nil = disabled cache (fast path in hot code) } if size > 100_000 { size = 100_000 // reasonable upper bound } return &LRU[K, V]{ size: size, items: make(map[K]*entry[K, V], size), onEvict: onEvict, } } // GetOrCompute retrieves a value or computes it if missing. // Ensures no double computation under concurrency. // Ideal for expensive computations like twwidth. func (c *LRU[K, V]) GetOrCompute(key K, compute func() V) V { if c == nil || c.size <= 0 { return compute() } c.mu.Lock() if e, ok := c.items[key]; ok { c.moveToFront(e) c.hits.Add(1) c.mu.Unlock() return e.value } c.misses.Add(1) value := compute() // expensive work only on real miss // Double-check: someone might have added it while computing if e, ok := c.items[key]; ok { e.value = value c.moveToFront(e) c.mu.Unlock() return value } // Evict if needed if len(c.items) >= c.size { c.removeOldest() } e := &entry[K, V]{key: key, value: value} c.addToFront(e) c.items[key] = e c.mu.Unlock() return value } // Get retrieves a value by key if it exists. // Returns the value and true if found, else zero and false. // Updates the entry to most recently used. func (c *LRU[K, V]) Get(key K) (V, bool) { if c == nil || c.size <= 0 { var zero V return zero, false } c.mu.Lock() defer c.mu.Unlock() e, ok := c.items[key] if !ok { c.misses.Add(1) var zero V return zero, false } c.hits.Add(1) c.moveToFront(e) return e.value, true } // Add inserts or updates a key-value pair. // Evicts the oldest if cache is full. // Returns true if an eviction occurred. func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { if c == nil || c.size <= 0 { return false } c.mu.Lock() defer c.mu.Unlock() if e, ok := c.items[key]; ok { e.value = value c.moveToFront(e) return false } if len(c.items) >= c.size { c.removeOldest() evicted = true } e := &entry[K, V]{key: key, value: value} c.addToFront(e) c.items[key] = e return evicted } // Remove deletes a key from the cache. // Returns true if the key was found and removed. func (c *LRU[K, V]) Remove(key K) bool { if c == nil || c.size <= 0 { return false } c.mu.Lock() defer c.mu.Unlock() e, ok := c.items[key] if !ok { return false } c.removeNode(e) delete(c.items, key) return true } // Purge clears all entries from the cache. // Calls onEvict for each entry if set. // Resets hit/miss counters. func (c *LRU[K, V]) Purge() { if c == nil || c.size <= 0 { return } c.mu.Lock() if c.onEvict != nil { for key, e := range c.items { c.onEvict(key, e.value) } } c.items = make(map[K]*entry[K, V], c.size) c.head = nil c.tail = nil c.hits.Store(0) c.misses.Store(0) c.mu.Unlock() } // Len returns the current number of items in the cache. func (c *LRU[K, V]) Len() int { if c == nil || c.size <= 0 { return 0 } c.mu.Lock() n := len(c.items) c.mu.Unlock() return n } // Cap returns the maximum capacity of the cache. func (c *LRU[K, V]) Cap() int { if c == nil { return 0 } return c.size } // HitRate returns the cache hit ratio (0.0 to 1.0). // Based on hits / (hits + misses). func (c *LRU[K, V]) HitRate() float64 { h := c.hits.Load() m := c.misses.Load() total := h + m if total == 0 { return 0.0 } return float64(h) / float64(total) } // RemoveOldest removes and returns the least recently used item. // Returns key, value, and true if an item was removed. // Calls onEvict if set. func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { if c == nil || c.size <= 0 { return } c.mu.Lock() defer c.mu.Unlock() if c.tail == nil { return } key = c.tail.key value = c.tail.value c.removeOldest() return key, value, true } // moveToFront moves an entry to the front (MRU position). func (c *LRU[K, V]) moveToFront(e *entry[K, V]) { if c.head == e { return } c.removeNode(e) c.addToFront(e) } // addToFront adds an entry to the front of the list. func (c *LRU[K, V]) addToFront(e *entry[K, V]) { e.prev = nil e.next = c.head if c.head != nil { c.head.prev = e } c.head = e if c.tail == nil { c.tail = e } } // removeNode removes an entry from the linked list. func (c *LRU[K, V]) removeNode(e *entry[K, V]) { if e.prev != nil { e.prev.next = e.next } else { c.head = e.next } if e.next != nil { e.next.prev = e.prev } else { c.tail = e.prev } e.prev = nil e.next = nil } // removeOldest removes the tail entry (LRU). // Calls onEvict if set and deletes from map. func (c *LRU[K, V]) removeOldest() { if c.tail == nil { return } e := c.tail if c.onEvict != nil { c.onEvict(e.key, e.value) } c.removeNode(e) delete(c.items, e.key) } tablewriter-1.1.4/pkg/twcache/lru_bench_test.go000066400000000000000000000032511515176644300216040ustar00rootroot00000000000000package twcache import ( "strconv" "testing" ) func BenchmarkLRU_GetHit(b *testing.B) { cache := NewLRU[string, int](100) for i := 0; i < 100; i++ { cache.Add(strconv.Itoa(i), i) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { cache.Get(strconv.Itoa(i % 100)) i++ } }) } func BenchmarkLRU_GetMiss(b *testing.B) { cache := NewLRU[string, int](100) for i := 0; i < 50; i++ { cache.Add(strconv.Itoa(i), i) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { cache.Get("miss_" + strconv.Itoa(i)) i++ } }) } func BenchmarkLRU_AddNew(b *testing.B) { cache := NewLRU[string, int](1000) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { cache.Add(strconv.Itoa(i), i) i++ } }) } func BenchmarkLRU_AddUpdate(b *testing.B) { cache := NewLRU[string, int](100) for i := 0; i < 100; i++ { cache.Add(strconv.Itoa(i), i) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { cache.Add(strconv.Itoa(i%100), i) i++ } }) } func BenchmarkLRU_GetOrCompute_Hit(b *testing.B) { cache := NewLRU[string, int](100) for i := 0; i < 100; i++ { cache.Add(strconv.Itoa(i), i) } compute := func() int { return 42 } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { cache.GetOrCompute(strconv.Itoa(i%100), compute) i++ } }) } func BenchmarkLRU_GetOrCompute_Miss(b *testing.B) { cache := NewLRU[string, int](1000) compute := func() int { return 42 } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { cache.GetOrCompute("key_"+strconv.Itoa(i), compute) i++ } }) } tablewriter-1.1.4/pkg/twcache/lru_test.go000066400000000000000000000130441515176644300204460ustar00rootroot00000000000000package twcache import ( "strconv" "sync" "testing" ) func TestLRU(t *testing.T) { t.Run("Basic Operations", func(t *testing.T) { cache := NewLRU[string, int](128) // Add cache.Add("key1", 100) val, ok := cache.Get("key1") if !ok || val != 100 { t.Errorf("expected 100, true; got %v, %v", val, ok) } // Update cache.Add("key1", 200) val, ok = cache.Get("key1") if !ok || val != 200 { t.Errorf("update failed: got %v, %v", val, ok) } // Miss _, ok = cache.Get("nonexistent") if ok { t.Error("miss should return ok=false") } }) t.Run("GetOrCompute", func(t *testing.T) { cache := NewLRU[string, int](10) counter := 0 compute := func() int { counter++ return 42 } v1 := cache.GetOrCompute("a", compute) if v1 != 42 || counter != 1 { t.Errorf("first call should compute: v=%d, counter=%d", v1, counter) } v2 := cache.GetOrCompute("a", compute) if v2 != 42 || counter != 1 { t.Errorf("second call should hit cache: v=%d, counter=%d", v2, counter) } // Concurrent safety var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() cache.GetOrCompute("b", compute) }() } wg.Wait() // Only one computation should have happened if counter != 2 { // "a" and "b" t.Errorf("expected exactly 2 computations under concurrency, got %d", counter) } }) t.Run("LRU Eviction", func(t *testing.T) { cache := NewLRU[string, int](2) cache.Add("key1", 1) cache.Add("key2", 2) // Make key1 most recent cache.Get("key1") // This should evict key2 cache.Add("key3", 3) if _, ok := cache.Get("key2"); ok { t.Error("key2 should have been evicted") } if v, ok := cache.Get("key1"); !ok || v != 1 { t.Error("key1 should still be present") } if v, ok := cache.Get("key3"); !ok || v != 3 { t.Error("key3 should be present") } }) t.Run("Eviction Callback", func(t *testing.T) { evicted := make([]string, 0) callback := func(key string, value int) { evicted = append(evicted, key) } cache := NewLRUEvict[string, int](2, callback) cache.Add("a", 1) cache.Add("b", 2) cache.Add("c", 3) // evicts "a" if len(evicted) != 1 || evicted[0] != "a" { t.Errorf("expected eviction of 'a', got %v", evicted) } cache.Purge() if len(evicted) != 3 { t.Errorf("Purge should call callback for all items, got %v", evicted) } }) t.Run("Disabled Cache (size <= 0)", func(t *testing.T) { cache := NewLRU[string, int](0) if cache != nil { t.Error("NewLRU(0) should return nil") } var nilCache *LRU[string, int] // All operations should be safe no-ops nilCache.Add("x", 1) nilCache.GetOrCompute("x", func() int { return 99 }) if v, ok := nilCache.Get("x"); ok || v != 0 { t.Errorf("nil cache Get should return (0, false), got %v, %v", v, ok) } nilCache.Purge() if nilCache.Len() != 0 || nilCache.Cap() != 0 { t.Error("nil cache should report zero size") } }) t.Run("Purge", func(t *testing.T) { cache := NewLRU[string, int](10) cache.Add("a", 1) cache.Add("b", 2) cache.Purge() if cache.Len() != 0 { t.Errorf("Len after Purge should be 0, got %d", cache.Len()) } if cache.HitRate() != 0 { t.Error("HitRate should reset after Purge") } }) t.Run("HitRate Tracking", func(t *testing.T) { cache := NewLRU[string, int](10) if r := cache.HitRate(); r != 0 { t.Errorf("initial hit rate should be 0, got %f", r) } cache.Get("miss1") // Miss (Total: 1) cache.Add("k", 1) // Add does NOT affect Hit/Miss count cache.Get("k") // Hit (Total: 2) cache.Get("miss2") // Miss (Total: 3) cache.Get("miss3") // Miss (Total: 4) <-- ADDED THIS LINE TO FIX TEST expected := 1.0 / 4.0 // 1 hit, 4 total accesses if r := cache.HitRate(); r != expected { t.Errorf("expected hit rate %.2f, got %.2f", expected, r) } }) t.Run("Concurrent Access", func(t *testing.T) { cache := NewLRU[string, int](1000) var wg sync.WaitGroup for i := 0; i < 200; i++ { wg.Add(1) go func(i int) { defer wg.Done() key := "key" + strconv.Itoa(i) cache.Add(key, i) }(i) } for i := 0; i < 200; i++ { wg.Add(1) go func(i int) { defer wg.Done() key := "key" + strconv.Itoa(i%100) cache.Get(key) }(i) } wg.Wait() if n := cache.Len(); n > 1000 { t.Errorf("cache size exceeded capacity: %d > 1000", n) } }) t.Run("MoveToFront Logic", func(t *testing.T) { cache := NewLRU[string, int](3) cache.Add("a", 1) cache.Add("b", 2) cache.Add("c", 3) cache.Get("b") // make b most recent cache.Add("d", 4) // should evict "a" if _, ok := cache.Get("a"); ok { t.Error("'a' should have been evicted") } for _, k := range []string{"b", "c", "d"} { if _, ok := cache.Get(k); !ok { t.Errorf("%s should be present", k) } } }) } // Add this to cache_test.go func TestCoverageGaps(t *testing.T) { t.Run("LRU Manual Removal", func(t *testing.T) { l := NewLRU[string, int](10) l.Add("a", 1) l.Add("b", 2) if !l.Remove("a") { t.Error("Remove('a') should return true") } if l.Len() != 1 { t.Errorf("Expected len 1, got %d", l.Len()) } if l.Remove("z") { t.Error("Remove('z') should return false") } key, val, ok := l.RemoveOldest() if !ok || key != "b" || val != 2 { t.Error("RemoveOldest should return 'b', 2") } _, _, ok = l.RemoveOldest() if ok { t.Error("RemoveOldest on empty should return false") } }) t.Run("LRU Safety Cap", func(t *testing.T) { l := NewLRU[string, int](1_000_000) if l.Cap() != 100_000 { t.Errorf("Expected capacity capped at 100,000, got %d", l.Cap()) } }) } tablewriter-1.1.4/pkg/twwarp/000077500000000000000000000000001515176644300161625ustar00rootroot00000000000000tablewriter-1.1.4/pkg/twwarp/_data/000077500000000000000000000000001515176644300172325ustar00rootroot00000000000000tablewriter-1.1.4/pkg/twwarp/_data/long-text-wrapped.txt000066400000000000000000000072071515176644300233620ustar00rootroot00000000000000Я к вам пишу — чего же боле? Что я могу еще сказать? Теперь, я знаю, в вашей воле Меня презреньем наказать. Но вы, к моей несчастной доле Хоть каплю жалости храня, Вы не оставите меня. Сначала я молчать хотела; Поверьте: моего стыда Вы не узнали б никогда, Когда б надежду я имела Хоть редко, хоть в неделю раз В деревне нашей видеть вас, Чтоб только слышать ваши речи, Вам слово молвить, и потом Все думать, думать об одном И день и ночь до новой встречи. Но, говорят, вы нелюдим; В глуши, в деревне всё вам скучно, А мы… ничем мы не блестим, Хоть вам и рады простодушно. Зачем вы посетили нас? В глуши забытого селенья Я никогда не знала б вас, Не знала б горького мученья. Души неопытной волненья Смирив со временем (как знать?), По сердцу я нашла бы друга, Была бы верная супруга И добродетельная мать. Другой!.. Нет, никому на свете Не отдала бы сердца я! То в вышнем суждено совете… То воля неба: я твоя; Вся жизнь моя была залогом Свиданья верного с тобой; Я знаю, ты мне послан богом, До гроба ты хранитель мой… Ты в сновиденьях мне являлся, Незримый, ты мне был уж мил, Твой чудный взгляд меня томил, В душе твой голос раздавался Давно… нет, это был не сон! Ты чуть вошел, я вмиг узнала, Вся обомлела, запылала И в мыслях молвила: вот он! Не правда ль? Я тебя слыхала: Ты говорил со мной в тиши, Когда я бедным помогала Или молитвой услаждала Тоску волнуемой души? И в это самое мгновенье Не ты ли, милое виденье, В прозрачной темноте мелькнул, Приникнул тихо к изголовью? Не ты ль, с отрадой и любовью, Слова надежды мне шепнул? Кто ты, мой ангел ли хранитель, Или коварный искуситель: Мои сомненья разреши. Быть может, это все пустое, Обман неопытной души! И суждено совсем иное… Но так и быть! Судьбу мою Отныне я тебе вручаю, Перед тобою слезы лью, Твоей защиты умоляю… Вообрази: я здесь одна, Никто меня не понимает, Рассудок мой изнемогает, И молча гибнуть я должна. Я жду тебя: единым взором Надежды сердца оживи Иль сон тяжелый перерви, Увы, заслуженным укором! Кончаю! Страшно перечесть… Стыдом и страхом замираю… Но мне порукой ваша честь, И смело ей себя вверяю…tablewriter-1.1.4/pkg/twwarp/_data/long-text.txt000066400000000000000000000234731515176644300217250ustar00rootroot00000000000000 Я к вам пишу — чего же боле? Что я могу еще сказать? Теперь, я знаю, в вашей воле Меня презреньем наказать. Но вы, к моей несчастной доле Хоть каплю жалости храня, Вы не оставите меня. Сначала я молчать хотела; Поверьте: моего стыда Вы не узнали б никогда, Когда б надежду я имела Хоть редко, хоть в неделю раз В деревне нашей видеть вас, Чтоб только слышать ваши речи, Вам слово молвить, и потом Все думать, думать об одном И день и ночь до новой встречи. Но, говорят, вы нелюдим; В глуши, в деревне всё вам скучно, А мы… ничем мы не блестим, Хоть вам и рады простодушно. Зачем вы посетили нас? В глуши забытого селенья Я никогда не знала б вас, Не знала б горького мученья. Души неопытной волненья Смирив со временем (как знать?), По сердцу я нашла бы друга, Была бы верная супруга И добродетельная мать. Другой!.. Нет, никому на свете Не отдала бы сердца я! То в вышнем суждено совете… То воля неба: я твоя; Вся жизнь моя была залогом Свиданья верного с тобой; Я знаю, ты мне послан богом, До гроба ты хранитель мой… Ты в сновиденьях мне являлся, Незримый, ты мне был уж мил, Твой чудный взгляд меня томил, В душе твой голос раздавался Давно… нет, это был не сон! Ты чуть вошел, я вмиг узнала, Вся обомлела, запылала И в мыслях молвила: вот он! Не правда ль? Я тебя слыхала: Ты говорил со мной в тиши, Когда я бедным помогала Или молитвой услаждала Тоску волнуемой души? И в это самое мгновенье Не ты ли, милое виденье, В прозрачной темноте мелькнул, Приникнул тихо к изголовью? Не ты ль, с отрадой и любовью, Слова надежды мне шепнул? Кто ты, мой ангел ли хранитель, Или коварный искуситель: Мои сомненья разреши. Быть может, это все пустое, Обман неопытной души! И суждено совсем иное… Но так и быть! Судьбу мою Отныне я тебе вручаю, Перед тобою слезы лью, Твоей защиты умоляю… Вообрази: я здесь одна, Никто меня не понимает, Рассудок мой изнемогает, И молча гибнуть я должна. Я жду тебя: единым взором Надежды сердца оживи Иль сон тяжелый перерви, Увы, заслуженным укором! Кончаю! Страшно перечесть… Стыдом и страхом замираю… Но мне порукой ваша честь, И смело ей себя вверяю… tablewriter-1.1.4/pkg/twwarp/wrap.go000066400000000000000000000137321515176644300174700ustar00rootroot00000000000000// 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/clipperhouse/uax29/v2/graphemes" "github.com/olekukonko/tablewriter/pkg/twwidth" ) 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 = twwidth.Width(v) 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 twwidth.Width(s) <= lim { return []string{s}, twwidth.Width(s) } // 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 := twwidth.Width(v) 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 := graphemes.FromString(s) for g.Next() { grapheme := g.Value() graphemeWidth := twwidth.Width(grapheme) if currentWidth+graphemeWidth > targetWidth { break } currentWidth += graphemeWidth endIndex = g.End() } 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] = twwidth.Width(words[i]) } 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.1.4/pkg/twwarp/wrap_test.go000066400000000000000000000113641515176644300205260ustar00rootroot00000000000000// 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" "os" "reflect" "runtime" "strings" "testing" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" ) 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.Error(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 twwidth.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 twwidth.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") s := string(d) b.SetBytes(int64(len(s))) b.ResetTimer() if err != nil { b.Fatal(err) } for i := 0; i < b.N; i++ { WrapString(s, 128) } } func BenchmarkWrapStringWithSpaces(b *testing.B) { d, err := os.ReadFile(testDir + "/long-text.txt") s := string(d) b.SetBytes(int64(len(s))) b.ResetTimer() if err != nil { b.Fatal(err) } for i := 0; i < b.N; i++ { WrapStringWithSpaces(s, 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.1.4/pkg/twwidth/000077500000000000000000000000001515176644300163305ustar00rootroot00000000000000tablewriter-1.1.4/pkg/twwidth/cache.go000066400000000000000000000011121515176644300177150ustar00rootroot00000000000000package twwidth import "github.com/olekukonko/tablewriter/pkg/twcache" // widthCache stores memoized results of Width calculations to improve performance. var widthCache *twcache.LRU[cacheKey, int] type cacheKey struct { eastAsian bool str string } // SetCacheCapacity changes the cache size dynamically // If capacity <= 0, disables caching entirely func SetCacheCapacity(capacity int) { mu.Lock() defer mu.Unlock() if capacity <= 0 { widthCache = nil // nil = fully disabled return } newCache := twcache.NewLRU[cacheKey, int](capacity) widthCache = newCache } tablewriter-1.1.4/pkg/twwidth/ea.go000066400000000000000000000263651515176644300172600ustar00rootroot00000000000000/* Package twwidth provides intelligent East Asian width detection. In 2025/2026, most modern terminal emulators (VSCode, Windows Terminal, iTerm2, Alacritty) and modern monospace fonts (Hack, Fira Code, Cascadia Code) treat box-drawing characters as Single Width, regardless of the underlying OS Locale. Detection Logic (in order of priority): - RUNEWIDTH_EASTASIAN environment variable (explicit user override) - Force Legacy Mode (programmatic override for backward compatibility) - Modern environment detection (VSCode, Windows Terminal, etc. -> Narrow) - Locale-based detection (CJK locales in traditional terminals -> Wide) This prioritization ensures that: - Users can always override behavior using RUNEWIDTH_EASTASIAN - Modern development environments work correctly by default - Traditional CJK terminals maintain compatibility via locale checks Examples: // Force narrow borders (for Hack font in zh_CN) RUNEWIDTH_EASTASIAN=0 go run . // Force wide borders (for legacy CJK terminals) RUNEWIDTH_EASTASIAN=1 go run . */ package twwidth import ( "os" "runtime" "strings" "sync" ) // Environment Variable Constants const ( EnvLCAll = "LC_ALL" EnvLCCtype = "LC_CTYPE" EnvLang = "LANG" EnvRuneWidthEastAsian = "RUNEWIDTH_EASTASIAN" EnvTerm = "TERM" EnvTermProgram = "TERM_PROGRAM" EnvTermProgramWsl = "TERM_PROGRAM_WSL" EnvWTProfile = "WT_PROFILE_ID" // Windows Terminal EnvConEmuANSI = "ConEmuANSI" // ConEmu EnvAlacritty = "ALACRITTY_LOG" // Alacritty EnvVTEVersion = "VTE_VERSION" // GNOME/VTE ) const ( overwriteOn = "override_on" overwriteOff = "override_off" envModern = "modern_env" envCjk = "locale_cjk" envAscii = "default_ascii" ) // CJK Language Codes (Prefixes) // Covers ISO 639-1 (2-letter) and common full names used in some systems. var cjkPrefixes = []string{ "zh", "ja", "ko", // Standard: Chinese, Japanese, Korean "chi", "zho", // ISO 639-2/B and T for Chinese "jpn", "kor", // ISO 639-2 for Japanese, Korean "chinese", "japanese", "korean", // Full names (rare but possible in some legacy systems) } // CJK Region Codes // Checks for specific regions that imply CJK font usage (e.g., en_HK). var cjkRegions = map[string]bool{ "cn": true, // China "tw": true, // Taiwan "hk": true, // Hong Kong "mo": true, // Macau "jp": true, // Japan "kr": true, // South Korea "kp": true, // North Korea "sg": true, // Singapore (Often uses CJK fonts) } // Modern environments that should use narrow borders (1-width box chars) var modernEnvironments = map[string]bool{ // Terminal programs "vscode": true, "visual studio code": true, "iterm.app": true, "iterm2": true, "windows terminal": true, "windowsterminal": true, "alacritty": true, "kitty": true, "hyper": true, "tabby": true, "terminus": true, "fluentterminal": true, "warp": true, "ghostty": true, "rio": true, "jetbrains-jediterm": true, // Terminal types (TERM signatures) "xterm-kitty": true, "xterm-ghostty": true, "wezterm": true, } var ( eastAsianOnce sync.Once eastAsianVal bool // Legacy override control // Renamed to cfgMu to avoid conflict with width.go's mu cfgMu sync.RWMutex forceLegacyEastAsian = false ) type Enviroment struct { GOOS string `json:"goos"` LC_ALL string `json:"lc_all"` LC_CTYPE string `json:"lc_ctype"` LANG string `json:"lang"` RUNEWIDTH_EASTASIAN string `json:"runewidth_eastasian"` TERM string `json:"term"` TERM_PROGRAM string `json:"term_program"` } // State captures the calculated internal state. type State struct { NormalizedLocale string `json:"normalized_locale"` IsCJKLocale bool `json:"is_cjk_locale"` IsModernEnv bool `json:"is_modern_env"` LegacyOverrideMode bool `json:"legacy_override_mode"` } // Detection aggregates all debug information regarding East Asian width detection. type Detection struct { AutoUseEastAsian bool `json:"auto_use_east_asian"` DetectionMode string `json:"detection_mode"` Raw Enviroment `json:"raw"` Derived State `json:"derived"` } // EastAsianForceLegacy forces the detection logic to ignore modern environment checks. // It relies solely on Locale detection. This is useful for applications that need // strict backward compatibility. // // Note: This does NOT override RUNEWIDTH_EASTASIAN. User environment variables take precedence. // This should be called before the first table render. func EastAsianForceLegacy(force bool) { cfgMu.Lock() defer cfgMu.Unlock() forceLegacyEastAsian = force } // EastAsianDetect checks the environment variables to determine if // East Asian width calculations should be enabled. func EastAsianDetect() bool { eastAsianOnce.Do(func() { eastAsianVal = detectEastAsian() }) return eastAsianVal } // EastAsianConservative is a stricter version that only defaults to Narrow // if the terminal is definitely known to be modern (e.g. VSCode, iTerm2). // It avoids heuristics like checking "xterm" in the TERM variable. func EastAsianConservative() bool { // Check overrides first if val, found := checkOverrides(); found { return val } // Stricter modern environment detection if isConservativeModernEnvironment() { return false } // Fall back to locale return checkLocale() } // EastAsianMode returns the decision path used for the current environment. // Useful for debugging why a specific width was chosen. func EastAsianMode() string { // Check override if val, found := checkOverrides(); found { if val { return overwriteOn } return overwriteOff } cfgMu.RLock() legacy := forceLegacyEastAsian cfgMu.RUnlock() if legacy { if checkLocale() { return envCjk } return envAscii } if isModernEnvironment() { return envModern } if checkLocale() { return envCjk } return envAscii } // Debugging returns detailed information about the detection decision. // Useful for users to include in Github issues. func Debugging() Detection { locale := getNormalizedLocale() cfgMu.RLock() legacy := forceLegacyEastAsian cfgMu.RUnlock() return Detection{ AutoUseEastAsian: EastAsianDetect(), DetectionMode: EastAsianMode(), Raw: Enviroment{ GOOS: runtime.GOOS, LC_ALL: os.Getenv(EnvLCAll), LC_CTYPE: os.Getenv(EnvLCCtype), LANG: os.Getenv(EnvLang), RUNEWIDTH_EASTASIAN: os.Getenv(EnvRuneWidthEastAsian), TERM: os.Getenv(EnvTerm), TERM_PROGRAM: os.Getenv(EnvTermProgram), }, Derived: State{ NormalizedLocale: locale, IsCJKLocale: isCJKLocale(locale), IsModernEnv: isModernEnvironment(), LegacyOverrideMode: legacy, }, } } // detectEastAsian evaluates the environment and locale settings to determine if East Asian width rules should apply. func detectEastAsian() bool { // User Override check (Highest Priority) if val, found := checkOverrides(); found { return val } // Force Legacy Mode check cfgMu.RLock() isLegacy := forceLegacyEastAsian cfgMu.RUnlock() if isLegacy { // Legacy mode ignores modern environment checks, // relying solely on locale. return checkLocale() } // Modern Environment Detection // If modern, we assume Single Width (return false) if isModernEnvironment() { return false } // 4. Locale Fallback return checkLocale() } // checkOverrides looks for RUNEWIDTH_EASTASIAN func checkOverrides() (bool, bool) { if rw := os.Getenv(EnvRuneWidthEastAsian); rw != "" { rw = strings.ToLower(rw) if rw == "0" || rw == "off" || rw == "false" || rw == "no" { return false, true } if rw == "1" || rw == "on" || rw == "true" || rw == "yes" { return true, true } } return false, false } // checkLocale performs the string analysis on LANG/LC_ALL func checkLocale() bool { locale := getNormalizedLocale() if locale == "" { return false } return isCJKLocale(locale) } // isModernEnvironment performs comprehensive checks for modern terminal capabilities. func isModernEnvironment() bool { // Check TERM_PROGRAM (Most reliable) if termProg := os.Getenv(EnvTermProgram); termProg != "" { termProgLower := strings.ToLower(termProg) if modernEnvironments[termProgLower] { return true } } // Check WSL specific variable if os.Getenv(EnvTermProgramWsl) != "" { return true } // Windows Specifics if runtime.GOOS == "windows" { // Windows Terminal if os.Getenv(EnvWTProfile) != "" { return true } // ConEmu/Cmder if os.Getenv(EnvConEmuANSI) == "ON" { return true } // Modern Windows console (Windows 10+) check via TERM if term := os.Getenv(EnvTerm); term != "" { termLower := strings.ToLower(term) if strings.Contains(termLower, "xterm") || strings.Contains(termLower, "vt") { return true } } } // VTE-based terminals (GNOME Terminal, Tilix, etc.) if os.Getenv(EnvVTEVersion) != "" { return true } // Check for Alacritty specifically if os.Getenv(EnvAlacritty) != "" { return true } // Check TERM for modern terminal signatures if term := os.Getenv(EnvTerm); term != "" { termLower := strings.ToLower(term) // Specific modern terminals often put their name in TERM if modernEnvironments[termLower] { return true } // Heuristics for standard modern-capable descriptors if strings.Contains(termLower, "xterm") && !strings.Contains(termLower, "xterm-mono") { return true } if strings.Contains(termLower, "screen") || strings.Contains(termLower, "tmux") { return true } } return false } // isConservativeModernEnvironment performs strict checks only for known modern terminals. func isConservativeModernEnvironment() bool { termProg := strings.ToLower(os.Getenv(EnvTermProgram)) // Allow-list of definitely modern terminals switch termProg { case "vscode", "visual studio code": return true case "iterm.app", "iterm2": return true case "windows terminal", "windowsterminal": return true case "alacritty", "wezterm", "kitty", "ghostty": return true case "warp", "tabby", "hyper": return true } // Windows Terminal via specific Env if os.Getenv(EnvWTProfile) != "" { return true } return false } // isCJKLocale determines if a given locale string corresponds to a CJK (Chinese, Japanese, Korean) language or region. func isCJKLocale(locale string) bool { // Check Language Prefix for _, prefix := range cjkPrefixes { if strings.HasPrefix(locale, prefix) { return true } } // Check Regions parts := strings.Split(locale, "_") if len(parts) > 1 { for _, part := range parts[1:] { if cjkRegions[part] { return true } } } return false } // getNormalizedLocale returns the normalized locale by inspecting environment variables LC_ALL, LC_CTYPE, and LANG. func getNormalizedLocale() string { var locale string if loc := os.Getenv(EnvLCAll); loc != "" { locale = loc } else if loc := os.Getenv(EnvLCCtype); loc != "" { locale = loc } else if loc := os.Getenv(EnvLang); loc != "" { locale = loc } // Fast fail for empty or standard C/POSIX locales if locale == "" || locale == "C" || locale == "POSIX" { return "" } // Strip encoding and modifiers if idx := strings.IndexByte(locale, '.'); idx != -1 { locale = locale[:idx] } if idx := strings.IndexByte(locale, '@'); idx != -1 { locale = locale[:idx] } return strings.ToLower(locale) } tablewriter-1.1.4/pkg/twwidth/ea_test.go000066400000000000000000000133771515176644300203160ustar00rootroot00000000000000package twwidth import ( "os" "testing" ) func TestDetectEastAsian_Logic(t *testing.T) { // We cannot run this in parallel (t.Parallel()) because it modifies // process-level environment variables and global state. // Helper struct to define the environment state for a test case type envConfig struct { lcAll string lcCtype string lang string runeWidth string // RUNEWIDTH_EASTASIAN termProg string // TERM_PROGRAM term string // TERM forceLegacy bool // Global legacy switch } tests := []struct { name string env envConfig expected bool }{ // LOCALE ONLY (Legacy Behavior / Non-Modern Terminal) { name: "Locale: Chinese (Simplified)", env: envConfig{lang: "zh_CN.UTF-8"}, expected: true, }, { name: "Locale: Japanese", env: envConfig{lang: "ja_JP.UTF-8"}, expected: true, }, { name: "Locale: English", env: envConfig{lang: "en_US.UTF-8"}, expected: false, }, { name: "Locale Priority: LC_ALL overrides LANG", env: envConfig{lcAll: "ja_JP", lang: "en_US"}, expected: true, }, { name: "Locale Region: English in Hong Kong (en_HK)", env: envConfig{lang: "en_HK"}, expected: true, }, // MODERN ENVIRONMENT (Should force Narrow/False) { name: "Modern: VSCode with Chinese Locale", env: envConfig{lang: "zh_CN.UTF-8", termProg: "vscode"}, expected: false, // Modern env implies single-width font }, { name: "Modern: iTerm2 with Japanese Locale", env: envConfig{lang: "ja_JP.UTF-8", termProg: "iTerm.app"}, expected: false, }, { name: "Modern: Windows Terminal via WT_PROFILE_ID (Simulated by TERM checks in this test structure)", // Note: We can't easily mock runtime.GOOS, so we stick to env vars // that work cross-platform in the heuristic function. // Let's test Alacritty which is checked via env var. env: envConfig{lang: "zh_CN.UTF-8", termProg: "Alacritty"}, expected: false, }, { name: "Modern: TERM=xterm-kitty with Chinese Locale", env: envConfig{lang: "zh_CN.UTF-8", term: "xterm-kitty"}, expected: false, }, // USER OVERRIDE (Highest Priority) { name: "Override: Force ON (1) in English Env", env: envConfig{lang: "en_US.UTF-8", runeWidth: "1"}, expected: true, }, { name: "Override: Force ON (true) overrides Modern Env", env: envConfig{lang: "en_US.UTF-8", termProg: "vscode", runeWidth: "true"}, expected: true, }, { name: "Override: Force OFF (0) in Chinese Env", env: envConfig{lang: "zh_CN.UTF-8", runeWidth: "0"}, expected: false, }, { name: "Override: Force OFF (false) overrides Legacy Detection", env: envConfig{lang: "zh_CN.UTF-8", runeWidth: "false"}, expected: false, }, // LEGACY FORCE SWITCH (Programmatic Override) { name: "Legacy Force: Ignores Modern Env", env: envConfig{lang: "zh_CN.UTF-8", termProg: "vscode", forceLegacy: true}, expected: true, // Should use Locale (True) despite VSCode }, { name: "Legacy Force: Still respects User Override (RW=0)", env: envConfig{lang: "zh_CN.UTF-8", runeWidth: "0", forceLegacy: true}, expected: false, // User env var is supreme }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Save current state saveEnv := func(key string) string { return os.Getenv(key) } oldLCAll := saveEnv(EnvLCAll) oldLCCtype := saveEnv(EnvLCCtype) oldLang := saveEnv(EnvLang) oldRuneWidth := saveEnv(EnvRuneWidthEastAsian) oldTermProg := saveEnv(EnvTermProgram) oldTerm := saveEnv(EnvTerm) // Helper to set/unset setEnv := func(key, val string) { if val == "" { os.Unsetenv(key) } else { os.Setenv(key, val) } } // Restore after test defer func() { setEnv(EnvLCAll, oldLCAll) setEnv(EnvLCCtype, oldLCCtype) setEnv(EnvLang, oldLang) setEnv(EnvRuneWidthEastAsian, oldRuneWidth) setEnv(EnvTermProgram, oldTermProg) setEnv(EnvTerm, oldTerm) EastAsianForceLegacy(false) // Reset global flag }() // Apply test configuration setEnv(EnvLCAll, tt.env.lcAll) setEnv(EnvLCCtype, tt.env.lcCtype) setEnv(EnvLang, tt.env.lang) setEnv(EnvRuneWidthEastAsian, tt.env.runeWidth) setEnv(EnvTermProgram, tt.env.termProg) setEnv(EnvTerm, tt.env.term) EastAsianForceLegacy(tt.env.forceLegacy) // Call internal logic directly to bypass sync.Once if got := detectEastAsian(); got != tt.expected { t.Errorf("detectEastAsian() = %v, want %v\nEnv: %+v", got, tt.expected, tt.env) } }) } } func TestAutoUseEastAsian_Cache(t *testing.T) { // This test verifies that the result is cached (sync.Once). // Since 'eastAsianOnce' is private and global, we assume this is the // first call in this process execution, OR we implicitly accept checking // the behavior of the *existing* singleton state if unrelated tests ran before. // IMPORTANT: Because other tests might have run, we can't guarantee `eastAsianOnce` // is fresh. This test is best effort to ensure stability. // Snapshot Result firstResult := EastAsianDetect() // Change Environmental factors to the OPPOSITE of what triggered firstResult if firstResult { // If currently True (e.g. CJK), try to force False os.Setenv(EnvLang, "en_US.UTF-8") os.Setenv(EnvRuneWidthEastAsian, "0") } else { // If currently False, try to force True os.Setenv(EnvLang, "zh_CN.UTF-8") os.Setenv(EnvRuneWidthEastAsian, "1") } // Ensure we clean up defer func() { os.Unsetenv(EnvLang) os.Unsetenv(EnvRuneWidthEastAsian) }() // Second call secondResult := EastAsianDetect() if firstResult != secondResult { t.Errorf("EastAsianDetect() did not cache result. First=%v, Second=%v", firstResult, secondResult) } } tablewriter-1.1.4/pkg/twwidth/tab.go000066400000000000000000000120651515176644300174310ustar00rootroot00000000000000package twwidth import ( "bufio" "os" "path/filepath" "strconv" "strings" "sync" ) type Tab rune const ( TabWidthDefault = 8 TabString Tab = '\t' ) // IsTab returns true if t equals the default tab. func (t Tab) IsTab() bool { return t == TabString } func (t Tab) Byte() byte { return byte(t) } func (t Tab) Rune() rune { return rune(t) } func (t Tab) String() string { return string(t) } // IsTab returns true if r is a tab rune. func IsTab(r rune) bool { return r == TabString.Rune() } type Tabinal struct { once sync.Once width int mu sync.RWMutex } func (t *Tabinal) String() string { return TabString.String() } // Size returns the current tab width, default if unset. func (t *Tabinal) Size() int { t.once.Do(t.init) t.mu.RLock() w := t.width t.mu.RUnlock() if w <= 0 { return TabWidthDefault } return w } // SetWidth sets the tab width if valid (1–32). func (t *Tabinal) SetWidth(w int) { if w <= 0 || w > 32 { return } t.mu.Lock() t.width = w t.mu.Unlock() } func (t *Tabinal) init() { w := t.detect() t.mu.Lock() t.width = w t.mu.Unlock() } // detect determines tab width using env, editorconfig, project, or term. func (t *Tabinal) detect() int { if w := envInt("TABWIDTH"); w > 0 { return clamp(w) } if w := envInt("TS"); w > 0 { return clamp(w) } if w := envInt("VIM_TABSTOP"); w > 0 { return clamp(w) } if w := editorConfigTabWidth(); w > 0 { return w } if w := projectHeuristic(); w > 0 { return w } if w := termHeuristic(); w > 0 { return w } return 0 } func editorConfigTabWidth() int { dir, err := os.Getwd() if err != nil { return 0 } for dir != "" && dir != "/" && dir != "." { path := filepath.Join(dir, ".editorconfig") if w := parseEditorConfig(path); w > 0 { return clamp(w) } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } return 0 } // parseEditorConfig reads tab_width or indent_size from a file. func parseEditorConfig(path string) int { f, err := os.Open(path) if err != nil { return 0 } defer f.Close() scanner := bufio.NewScanner(f) inMatch := false globalWidth := 0 for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { continue } if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { pattern := line[1 : len(line)-1] inMatch = pattern == "*" knownExts := []string{".go", ".py", ".js", ".ts", ".java", ".rs"} for _, ext := range knownExts { if strings.Contains(pattern, ext) { inMatch = true break } } continue } if !inMatch && globalWidth == 0 { continue } parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) val := strings.TrimSpace(parts[1]) switch key { case "tab_width": if w, err := strconv.Atoi(val); err == nil && w > 0 { if inMatch { return clamp(w) } if globalWidth == 0 { globalWidth = w } } case "indent_size": if val == "tab" { continue } if w, err := strconv.Atoi(val); err == nil && w > 0 { if inMatch { return clamp(w) } if globalWidth == 0 { globalWidth = w } } } } return globalWidth } // projectHeuristic returns 4 for known project types. func projectHeuristic() int { dir, err := os.Getwd() if err != nil { return 0 } indicators := []string{ "go.mod", "go.sum", "package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "setup.py", "pyproject.toml", "requirements.txt", "Pipfile", "pom.xml", "build.gradle", "build.gradle.kts", "Cargo.toml", "composer.json", } for _, indicator := range indicators { if _, err := os.Stat(filepath.Join(dir, indicator)); err == nil { return 4 } } patterns := []string{"*.go", "*.py", "*.js", "*.ts", "*.java", "*.rs"} for _, pattern := range patterns { if matches, _ := filepath.Glob(filepath.Join(dir, pattern)); len(matches) > 0 { return 4 } } return 0 } // termHeuristic returns a default width based on the TERM variable. func termHeuristic() int { termEnv := strings.ToLower(os.Getenv("TERM")) if termEnv == "" { return 0 } if strings.Contains(termEnv, "vt52") { return 2 } if strings.Contains(termEnv, "xterm") || strings.Contains(termEnv, "screen") || strings.Contains(termEnv, "tmux") || strings.Contains(termEnv, "linux") || strings.Contains(termEnv, "ansi") || strings.Contains(termEnv, "rxvt") { return TabWidthDefault } return 0 } func clamp(w int) int { if w <= 0 { return 0 } if w > 32 { return 32 } return w } var ( globalTab *Tabinal globalTabOnce sync.Once ) // TabInstance returns the singleton Tabinal. func TabInstance() *Tabinal { globalTabOnce.Do(func() { globalTab = &Tabinal{} }) return globalTab } // TabWidth returns the detected global tab width. func TabWidth() int { return TabInstance().Size() } // SetTabWidth sets the global tab width. func SetTabWidth(w int) { TabInstance().SetWidth(w) } func envInt(k string) int { v := os.Getenv(k) w, _ := strconv.Atoi(v) return w } tablewriter-1.1.4/pkg/twwidth/width.go000066400000000000000000000305721515176644300200050ustar00rootroot00000000000000package twwidth import ( "bytes" "regexp" "strings" "sync" "github.com/clipperhouse/displaywidth" "github.com/mattn/go-runewidth" "github.com/olekukonko/tablewriter/pkg/twcache" ) const ( cacheCapacity = 8192 cachePrefix = "0:" cacheEastAsianPrefix = "1:" ) // Options allows for configuring width calculation on a per-call basis. type Options struct { EastAsianWidth bool // Explicitly force box drawing chars to be narrow // regardless of EastAsianWidth setting. ForceNarrowBorders bool } // globalOptions holds the global displaywidth configuration, including East Asian width settings. var globalOptions Options // mu protects access to globalOptions 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() { isEastAsian := EastAsianDetect() cond := runewidth.NewCondition() cond.EastAsianWidth = isEastAsian globalOptions = Options{ EastAsianWidth: isEastAsian, // Auto-enable ForceNarrowBorders for edge cases. // If EastAsianWidth is ON (e.g. forced via Env Var), but we detect // a modern environment, we might technically want to narrow borders // while keeping text wide. ForceNarrowBorders: isEastAsian && isModernEnvironment(), } widthCache = twcache.NewLRU[cacheKey, int](cacheCapacity) } // Display calculates the visual width of a string using a specific runewidth.Condition. // Deprecated: use WidthWithOptions with the new twwidth.Options struct instead. // This function is kept for backward compatibility. func Display(cond *runewidth.Condition, str string) int { opts := Options{EastAsianWidth: cond.EastAsianWidth} return WidthWithOptions(str, opts) } // 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 { regESC := "\x1b" // ASCII escape character regBEL := "\x07" // ASCII bell character // ANSI string terminator: either ESC+\ or BEL regST := "(" + regexp.QuoteMeta(regESC+"\\") + "|" + regexp.QuoteMeta(regBEL) + ")" // Control Sequence Introducer (CSI): ESC[ followed by parameters and a final byte regCSI := regexp.QuoteMeta(regESC+"[") + "[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]" // Operating System Command (OSC): ESC] followed by arbitrary content until a terminator regOSC := regexp.QuoteMeta(regESC+"]") + ".*?" + regST // Combine CSI and OSC patterns into a single regex return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")") } // GetCacheStats returns current cache statistics func GetCacheStats() (size, capacity int, hitRate float64) { mu.Lock() defer mu.Unlock() if widthCache == nil { return 0, 0, 0 } return widthCache.Len(), widthCache.Cap(), widthCache.HitRate() } // IsEastAsian returns the current East Asian width setting. // This function is thread-safe. // // Example: // // if twdw.IsEastAsian() { // // Handle East Asian width characters // } func IsEastAsian() bool { mu.Lock() defer mu.Unlock() return globalOptions.EastAsianWidth } // SetCondition sets the global East Asian width setting based on a runewidth.Condition. // Deprecated: use SetOptions with the new twwidth.Options struct instead. // This function is kept for backward compatibility. func SetCondition(cond *runewidth.Condition) { mu.Lock() defer mu.Unlock() newEastAsianWidth := cond.EastAsianWidth if globalOptions.EastAsianWidth != newEastAsianWidth { globalOptions.EastAsianWidth = newEastAsianWidth widthCache.Purge() } } // SetEastAsian enables or disables East Asian width handling globally. // This function is thread-safe. // // Example: // // twdw.SetEastAsian(true) // Enable East Asian width handling func SetEastAsian(enable bool) { SetOptions(Options{EastAsianWidth: enable}) } // SetForceNarrow to preserve the new flag, or create a new setter func SetForceNarrow(enable bool) { mu.Lock() defer mu.Unlock() globalOptions.ForceNarrowBorders = enable widthCache.Purge() // Clear cache because widths might change } // SetOptions sets the global options for width calculation. // This function is thread-safe. func SetOptions(opts Options) { mu.Lock() defer mu.Unlock() if globalOptions.EastAsianWidth != opts.EastAsianWidth || globalOptions.ForceNarrowBorders != opts.ForceNarrowBorders { globalOptions = opts widthCache.Purge() } } // 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 the string contains ANSI, we must ensure it ends with a reset // to prevent bleeding, even if we don't truncate. safeS := s if strings.Contains(s, "\x1b") && !strings.HasSuffix(s, "\x1b[0m") { safeS += "\x1b[0m" } if len(suffixStr) == 0 { // No suffix. return safeS } // Suffix is provided. Check if s + suffix fits. if sDisplayWidth+suffixDisplayWidth <= maxWidth { return safeS + suffixStr } // s fits, but s + suffix is too long. Return s (with reset if needed). return safeS } // Case 4: String needs truncation (sDisplayWidth > maxWidth). // maxWidth is the total budget for the final string (content + suffix). mu.Lock() currentOpts := globalOptions mu.Unlock() // Special case for EastAsianDetect true: if only suffix fits, return suffix. // This was derived from previous test behavior. if len(suffixStr) > 0 && currentOpts.EastAsianWidth { provisionalContentWidth := maxWidth - suffixDisplayWidth if provisionalContentWidth == 0 { // Exactly enough space for suffix only return suffixStr } } // 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. } var contentBuf bytes.Buffer var currentContentDisplayWidth int var ansiSeqBuf bytes.Buffer inAnsiSequence := false ansiWrittenToContent := false 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] switch introducer { case '[': if seqLen >= 3 && r >= 0x40 && r <= 0x7E { terminated = true } case ']': 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 := calculateRunewidth(r, currentOpts) if targetContentForIteration == 0 { // No budget for content at all break } if currentContentDisplayWidth+runeDisplayWidth > targetContentForIteration { break } contentBuf.WriteRune(r) currentContentDisplayWidth += runeDisplayWidth } } result := contentBuf.String() // Determine if we need to append a reset sequence to prevent color bleeding. // This is needed if we wrote any ANSI codes or if the input had active codes // that we might have cut off or left open. needsReset := false 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[") { needsReset = true } if needsReset { result += "\x1b[0m" } // Suffix is added if provided. if len(suffixStr) > 0 { result += suffixStr } return result } // Width calculates the visual width of a string using the global cache for performance. // It excludes ANSI escape sequences and accounts for the global East Asian width setting. // This function is thread-safe. // // Example: // // width := twdw.Width("Hello\x1b[31mWorld") // Returns 10 func Width(str string) int { // Fast path ASCII (Optimization) if len(str) == 1 && str[0] < 0x80 { // Treat tab as special case even in fast path if IsTab(rune(str[0])) { return TabWidth() } return 1 } mu.Lock() currentOpts := globalOptions mu.Unlock() key := cacheKey{ eastAsian: currentOpts.EastAsianWidth, str: str, } // Check Cache (Optimization) if w, found := widthCache.Get(key); found { return w } //stripped := ansi.ReplaceAllLiteralString(str, "") calculatedWidth := 0 for _, r := range strip(str) { calculatedWidth += calculateRunewidth(r, currentOpts) } // Store in Cache widthCache.Add(key, calculatedWidth) return calculatedWidth } // WidthNoCache calculates the visual width of a string without using the global cache. // // Example: // // width := twdw.WidthNoCache("Hello\x1b[31mWorld") // Returns 10 func WidthNoCache(str string) int { // This function's behavior is equivalent to a one-shot calculation // using the current global options. The WidthWithOptions function // does not interact with the cache, thus fulfilling the requirement. mu.Lock() opts := globalOptions mu.Unlock() return WidthWithOptions(str, opts) } // WidthWithOptions calculates the visual width of a string with specific options, // bypassing the global settings and cache. This is useful for one-shot calculations // where global state is not desired. func WidthWithOptions(str string, opts Options) int { // stripped := ansi.ReplaceAllLiteralString(str, "") calculatedWidth := 0 for _, r := range strip(str) { calculatedWidth += calculateRunewidth(r, opts) } return calculatedWidth } // calculateRunewidth calculates the width of a single rune based on the provided options. // It applies narrow overrides for box drawing characters if configured and handles Tabs. func calculateRunewidth(r rune, opts Options) int { if opts.ForceNarrowBorders && isBoxDrawingChar(r) { return 1 } // Explicitly handle Tabinal to ensure tables have enough space // when TrimTab is Off. if IsTab(r) { return TabWidth() } dwOpts := displaywidth.Options{EastAsianWidth: opts.EastAsianWidth} return dwOpts.Rune(r) } // isBoxDrawingChar checks if a rune is within the Unicode Box Drawing range. func isBoxDrawingChar(r rune) bool { return r >= 0x2500 && r <= 0x257F } func strip(s string) string { if strings.IndexByte(s, '\x1b') == -1 { return s } return ansi.ReplaceAllLiteralString(s, "") } tablewriter-1.1.4/pkg/twwidth/width_bench_test.go000066400000000000000000000031641515176644300222000ustar00rootroot00000000000000package twwidth import ( "fmt" "strconv" "strings" "testing" ) 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.SetBytes(int64(len(str))) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = WidthNoCache(str) } }) b.Run(fmt.Sprintf("%s_EA%v_CacheMiss", name, eaSetting), func(b *testing.B) { b.SetBytes(int64(len(str))) b.ReportAllocs() b.ResetTimer() 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.SetBytes(int64(len(str))) b.ReportAllocs() b.ResetTimer() if b.N > 0 { _ = Width(str) } for i := 1; i < b.N; i++ { _ = Width(str) } }) resetGlobalCache() } } } tablewriter-1.1.4/pkg/twwidth/width_test.go000066400000000000000000000235251515176644300210440ustar00rootroot00000000000000package twwidth import ( "fmt" "os" "os/exec" "strings" "sync" "testing" "github.com/mattn/go-runewidth" "github.com/olekukonko/tablewriter/pkg/twcache" ) func helperProcess() { if IsEastAsian() { fmt.Fprint(os.Stdout, "true") } else { fmt.Fprint(os.Stdout, "false") } } func resetGlobalCache() { mu.Lock() widthCache = twcache.NewLRU[cacheKey, int](cacheCapacity) mu.Unlock() } func TestMain(m *testing.M) { if os.Getenv("GO_TEST_SUBPROCESS") == "1" { helperProcess() return } os.Exit(m.Run()) } func TestInitRespectsEnvironment(t *testing.T) { testCases := []struct { name string envVar string wantOutput string }{ {"RUNEWIDTH_EASTASIAN=1", "1", "true"}, {"RUNEWIDTH_EASTASIAN=0", "0", "false"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cmd := exec.Command(os.Args[0], "-test.run=^TestHelperProcess$") cmd.Env = append(os.Environ(), "GO_TEST_SUBPROCESS=1", "RUNEWIDTH_EASTASIAN="+tc.envVar) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("command failed: %v\nOutput: %s", err, output) } got := strings.TrimSpace(string(output)) if got != tc.wantOutput { t.Errorf("with RUNEWIDTH_EASTASIAN=%s, IsEastAsian() was %s, want %s", tc.envVar, got, tc.wantOutput) } }) } } 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 := IsEastAsian() t.Cleanup(func() { SetEastAsian(original) }) SetEastAsian(true) if !IsEastAsian() { t.Errorf("SetEastAsian(true): IsEastAsian() = false, want true") } SetEastAsian(false) if IsEastAsian() { t.Errorf("SetEastAsian(false): IsEastAsian() = true, want false") } } 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, options) = %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 TestCoverageWidth(t *testing.T) { t.Run("Direct Width Functions", func(t *testing.T) { w := WidthNoCache("abc") if w != 3 { t.Errorf("WidthNoCache('abc') = %d, want 3", w) } opts := Options{EastAsianWidth: true} w2 := WidthWithOptions("abc", opts) if w2 != 3 { t.Errorf("WidthWithOptions result wrong") } }) t.Run("SetCondition", func(t *testing.T) { original := IsEastAsian() defer SetEastAsian(original) cond := &runewidth.Condition{EastAsianWidth: !original} SetCondition(cond) if IsEastAsian() == original { t.Error("SetCondition failed to update global state") } }) t.Run("Truncate ANSI Reset", func(t *testing.T) { input := "\x1b[31mHello World" got := Truncate(input, 5) if !strings.HasPrefix(got, "\x1b[31m") { t.Error("Lost initial color code") } if !strings.HasSuffix(got, "\x1b[0m") { t.Errorf("Expected ANSI reset at end, got: %q", got) } got = Truncate("\x1b[31mHi", 2) if !strings.HasSuffix(got, "\x1b[0m") { t.Error("Should append reset even on exact fit if color was active") } }) t.Run("Truncate Suffix Logic", func(t *testing.T) { got := Truncate("Hello", 2, "...") if got != "" { t.Errorf("Expected empty string when suffix doesn't fit, got %q", got) } got = Truncate("Hello", 3, "...") if got != "..." { t.Errorf("Expected only suffix, got %q", got) } }) t.Run("Global Cache Management", func(t *testing.T) { mu.Lock() origCache := widthCache mu.Unlock() defer func() { mu.Lock() widthCache = origCache mu.Unlock() }() SetCacheCapacity(0) size, cap, _ := GetCacheStats() if size != 0 || cap != 0 { t.Error("Cache should be disabled (stats 0,0)") } if w := Width("abc"); w != 3 { t.Errorf("Width('abc') = %d, want 3", w) } SetCacheCapacity(10) Width("ab") size, cap, _ = GetCacheStats() if size != 1 || cap != 10 { t.Errorf("Stats mismatch: size=%d, cap=%d", size, cap) } }) } func TestBug_ForceNarrow_MultiChar(t *testing.T) { // Setup the specific condition causing the bug: // EastAsianWidth is ON (simulating RUNEWIDTH_EASTASIAN=1) // ForceNarrowBorders is ON (simulating Modern Terminal detection) buggyOptions := Options{ EastAsianWidth: true, ForceNarrowBorders: true, } SetOptions(buggyOptions) // Define a border string commonly used by the renderer (length > 1) // The current bug fails because it only checks len(r) == 1 input := "─────" // 5 chars want := 5 // Should be 5 width (1 per char) // 3. Measure got := Width(input) // 4. Assert if got != want { t.Fatalf("Regression found: ForceNarrowBorders failed on multi-char string.\nInput: %q\nGot Width: %d (Likely Double Width)\nWant Width: %d", input, got, want) } } tablewriter-1.1.4/renderer/000077500000000000000000000000001515176644300156635ustar00rootroot00000000000000tablewriter-1.1.4/renderer/blueprint.go000066400000000000000000000525521515176644300202270ustar00rootroot00000000000000package renderer import ( "io" "strings" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" ) // Blueprint implements a primary table rendering engine with customizable borders and alignments. type Blueprint struct { config tw.Rendition // Rendering configuration for table borders and symbols logger *ll.Logger // Logger for debug trace messages w io.Writer } // NewBlueprint creates a new Blueprint instance with optional custom configurations. func NewBlueprint(configs ...tw.Rendition) *Blueprint { // Initialize with default configuration cfg := defaultBlueprint() if len(configs) > 0 { userCfg := configs[0] // Override default borders if provided if userCfg.Borders.Left != 0 { cfg.Borders.Left = userCfg.Borders.Left } if userCfg.Borders.Right != 0 { cfg.Borders.Right = userCfg.Borders.Right } if userCfg.Borders.Top != 0 { cfg.Borders.Top = userCfg.Borders.Top } if userCfg.Borders.Bottom != 0 { cfg.Borders.Bottom = userCfg.Borders.Bottom } // Override symbols if provided if userCfg.Symbols != nil { cfg.Symbols = userCfg.Symbols } // Merge user settings with default settings cfg.Settings = mergeSettings(cfg.Settings, userCfg.Settings) } return &Blueprint{config: cfg, logger: ll.New("blueprint").Disable()} } // Close performs cleanup (no-op in this implementation). func (f *Blueprint) Close() error { f.logger.Debug("Blueprint.Close() called (no-op).") return nil } // Config returns the renderer's current configuration. func (f *Blueprint) Config() tw.Rendition { return f.config } // Footer renders the table footer section with configured formatting. func (f *Blueprint) Footer(footers [][]string, ctx tw.Formatting) { f.logger.Debugf("Starting Footer render: IsSubRow=%v, Location=%v, Pos=%s", ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position) // Render the footer line f.renderLine(ctx) f.logger.Debug("Completed Footer render") } // Header renders the table header section with configured formatting. func (f *Blueprint) Header(headers [][]string, ctx tw.Formatting) { f.logger.Debugf("Starting Header render: IsSubRow=%v, Location=%v, Pos=%s, lines=%d, widths=%v", ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, len(ctx.Row.Current), ctx.Row.Widths) // Render the header line f.renderLine(ctx) f.logger.Debug("Completed Header render") } // Line renders a full horizontal row line with junctions and segments. func (f *Blueprint) Line(ctx tw.Formatting) { // Initialize junction renderer jr := NewJunction(JunctionContext{ Symbols: f.config.Symbols, Ctx: ctx, ColIdx: 0, Logger: f.logger, BorderTint: Tint{}, SeparatorTint: Tint{}, }) var line strings.Builder totalLineWidth := 0 // Track total display width // Get sorted column indices sortedKeys := ctx.Row.Widths.SortedKeys() numCols := 0 if len(sortedKeys) > 0 { numCols = sortedKeys[len(sortedKeys)-1] + 1 } // Handle empty row case if numCols == 0 { prefix := tw.Empty suffix := tw.Empty if f.config.Borders.Left.Enabled() { prefix = jr.RenderLeft() } if f.config.Borders.Right.Enabled() { suffix = jr.RenderRight(-1) } if prefix != tw.Empty || suffix != tw.Empty { line.WriteString(prefix + suffix + tw.NewLine) totalLineWidth = twwidth.Width(prefix) + twwidth.Width(suffix) f.w.Write([]byte(line.String())) } f.logger.Debugf("Line: Handled empty row/widths case (total width %d)", totalLineWidth) return } // Calculate target total width based on data rows targetTotalWidth := 0 for _, colIdx := range sortedKeys { targetTotalWidth += ctx.Row.Widths.Get(colIdx) } if f.config.Borders.Left.Enabled() { targetTotalWidth += twwidth.Width(f.config.Symbols.Column()) } if f.config.Borders.Right.Enabled() { targetTotalWidth += twwidth.Width(f.config.Symbols.Column()) } if f.config.Settings.Separators.BetweenColumns.Enabled() && len(sortedKeys) > 1 { targetTotalWidth += twwidth.Width(f.config.Symbols.Column()) * (len(sortedKeys) - 1) } // Add left border if enabled leftBorderWidth := 0 if f.config.Borders.Left.Enabled() { leftBorder := jr.RenderLeft() line.WriteString(leftBorder) leftBorderWidth = twwidth.Width(leftBorder) totalLineWidth += leftBorderWidth f.logger.Debugf("Line: Left border='%s' (f.width %d)", leftBorder, leftBorderWidth) } visibleColIndices := make([]int, 0) // Calculate visible columns for _, colIdx := range sortedKeys { colWidth := ctx.Row.Widths.Get(colIdx) if colWidth > 0 { visibleColIndices = append(visibleColIndices, colIdx) } } f.logger.Debugf("Line: sortedKeys=%v, Widths=%v, visibleColIndices=%v, targetTotalWidth=%d", sortedKeys, ctx.Row.Widths, visibleColIndices, targetTotalWidth) // Render each column segment for keyIndex, currentColIdx := range visibleColIndices { jr.colIdx = currentColIdx segment := jr.GetSegment() colWidth := ctx.Row.Widths.Get(currentColIdx) // Adjust colWidth to account for wider borders adjustedColWidth := colWidth if f.config.Borders.Left.Enabled() && keyIndex == 0 { adjustedColWidth -= leftBorderWidth - twwidth.Width(f.config.Symbols.Column()) } if f.config.Borders.Right.Enabled() && keyIndex == len(visibleColIndices)-1 { rightBorderWidth := twwidth.Width(jr.RenderRight(currentColIdx)) adjustedColWidth -= rightBorderWidth - twwidth.Width(f.config.Symbols.Column()) } if adjustedColWidth < 0 { adjustedColWidth = 0 } f.logger.Debugf("Line: colIdx=%d, segment='%s', adjusted colWidth=%d", currentColIdx, segment, adjustedColWidth) if segment == tw.Empty { spaces := strings.Repeat(tw.Space, adjustedColWidth) line.WriteString(spaces) totalLineWidth += adjustedColWidth f.logger.Debugf("Line: Rendered spaces='%s' (f.width %d) for col %d", spaces, adjustedColWidth, currentColIdx) } else { segmentWidth := twwidth.Width(segment) if segmentWidth == 0 { segmentWidth = 1 // Avoid division by zero f.logger.Warnf("Line: Segment='%s' has zero width, using 1", segment) } // Calculate how many full segments fit repeat := adjustedColWidth / segmentWidth if repeat < 1 && adjustedColWidth > 0 { repeat = 1 } repeatedSegment := strings.Repeat(segment, repeat) actualWidth := twwidth.Width(repeatedSegment) if actualWidth > adjustedColWidth { // Truncate if too long repeatedSegment = twwidth.Truncate(repeatedSegment, adjustedColWidth) actualWidth = twwidth.Width(repeatedSegment) f.logger.Debugf("Line: Truncated segment='%s' to width %d", repeatedSegment, actualWidth) } else if actualWidth < adjustedColWidth { // Pad with segment character to match adjustedColWidth remainingWidth := adjustedColWidth - actualWidth for i := 0; i < remainingWidth/segmentWidth; i++ { repeatedSegment += segment } actualWidth = twwidth.Width(repeatedSegment) if actualWidth < adjustedColWidth { repeatedSegment = tw.PadRight(repeatedSegment, tw.Space, adjustedColWidth) actualWidth = adjustedColWidth f.logger.Debugf("Line: Padded segment with spaces='%s' to width %d", repeatedSegment, actualWidth) } f.logger.Debugf("Line: Padded segment='%s' to width %d", repeatedSegment, actualWidth) } line.WriteString(repeatedSegment) totalLineWidth += actualWidth f.logger.Debugf("Line: Rendered segment='%s' (f.width %d) for col %d", repeatedSegment, actualWidth, currentColIdx) } // Add junction between columns if not the last column isLast := keyIndex == len(visibleColIndices)-1 if !isLast && f.config.Settings.Separators.BetweenColumns.Enabled() { nextColIdx := visibleColIndices[keyIndex+1] junction := jr.RenderJunction(currentColIdx, nextColIdx) // Use center symbol (❀) or column separator (|) to match data rows if twwidth.Width(junction) != twwidth.Width(f.config.Symbols.Column()) { junction = f.config.Symbols.Center() if twwidth.Width(junction) != twwidth.Width(f.config.Symbols.Column()) { junction = f.config.Symbols.Column() } } junctionWidth := twwidth.Width(junction) line.WriteString(junction) totalLineWidth += junctionWidth f.logger.Debugf("Line: Junction between %d and %d: '%s' (f.width %d)", currentColIdx, nextColIdx, junction, junctionWidth) } } // Add right border rightBorderWidth := 0 if f.config.Borders.Right.Enabled() && len(visibleColIndices) > 0 { lastIdx := visibleColIndices[len(visibleColIndices)-1] rightBorder := jr.RenderRight(lastIdx) rightBorderWidth = twwidth.Width(rightBorder) line.WriteString(rightBorder) totalLineWidth += rightBorderWidth f.logger.Debugf("Line: Right border='%s' (f.width %d)", rightBorder, rightBorderWidth) } // Write the final line line.WriteString(tw.NewLine) f.w.Write([]byte(line.String())) f.logger.Debugf("Line rendered: '%s' (total width %d, target %d)", strings.TrimSuffix(line.String(), tw.NewLine), totalLineWidth, targetTotalWidth) } // Logger sets the logger for the Blueprint instance. func (f *Blueprint) Logger(logger *ll.Logger) { f.logger = logger.Namespace("blueprint") } // Row renders a table data row with configured formatting. func (f *Blueprint) Row(row []string, ctx tw.Formatting) { f.logger.Debugf("Starting Row render: IsSubRow=%v, Location=%v, Pos=%s, hasFooter=%v", ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, ctx.HasFooter) // Render the row line f.renderLine(ctx) f.logger.Debug("Completed Row render") } // Start initializes the rendering process (no-op in this implementation). func (f *Blueprint) Start(w io.Writer) error { f.w = w f.logger.Debug("Blueprint.Start() called (no-op).") return nil } // formatCell formats a cell's content with specified width, padding, and alignment, returning an empty string if width is non-positive. func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, align tw.Align) string { if width <= 0 { return tw.Empty } f.logger.Debugf("Formatting cell: content='%s', width=%d, align=%s, padding={L:'%s' R:'%s'}", content, width, align, padding.Left, padding.Right) // Calculate display width of content runeWidth := twwidth.Width(content) // Set default padding characters leftPadChar := padding.Left rightPadChar := padding.Right // if f.config.Settings.Cushion.Enabled() || f.config.Settings.Cushion.Default() { // if leftPadChar == tw.Empty { // leftPadChar = tw.Space // } // if rightPadChar == tw.Empty { // rightPadChar = tw.Space // } //} // Calculate padding widths padLeftWidth := twwidth.Width(leftPadChar) padRightWidth := twwidth.Width(rightPadChar) // Calculate available width for content availableContentWidth := max(width-padLeftWidth-padRightWidth, 0) f.logger.Debugf("Available content width: %d", availableContentWidth) // Truncate content if it exceeds available width if runeWidth > availableContentWidth { content = twwidth.Truncate(content, availableContentWidth) runeWidth = twwidth.Width(content) f.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableContentWidth, content, runeWidth) } // Calculate total padding needed totalPaddingWidth := max(width-runeWidth, 0) f.logger.Debugf("Total padding width: %d", totalPaddingWidth) var result strings.Builder var leftPaddingWidth, rightPaddingWidth int // Apply alignment and padding switch align { case tw.AlignLeft: result.WriteString(leftPadChar) result.WriteString(content) rightPaddingWidth = totalPaddingWidth - padLeftWidth if rightPaddingWidth > 0 { padChar := rightPadChar if padChar == tw.Empty { padChar = tw.Space } result.WriteString(tw.PadRight(tw.Empty, padChar, rightPaddingWidth)) f.logger.Debugf("Applied right padding: '%s' for %d width", padChar, rightPaddingWidth) } case tw.AlignRight: leftPaddingWidth = totalPaddingWidth - padRightWidth if leftPaddingWidth > 0 { padChar := leftPadChar if padChar == tw.Empty { padChar = tw.Space } result.WriteString(tw.PadLeft(tw.Empty, padChar, leftPaddingWidth)) f.logger.Debugf("Applied left padding: '%s' for %d width", padChar, leftPaddingWidth) } result.WriteString(content) result.WriteString(rightPadChar) case tw.AlignCenter: leftPaddingWidth = (totalPaddingWidth-padLeftWidth-padRightWidth)/2 + padLeftWidth rightPaddingWidth = totalPaddingWidth - leftPaddingWidth if leftPaddingWidth > padLeftWidth { padChar := leftPadChar if padChar == tw.Empty { padChar = tw.Space } result.WriteString(tw.PadLeft(tw.Empty, padChar, leftPaddingWidth-padLeftWidth)) f.logger.Debugf("Applied left centering padding: '%s' for %d width", padChar, leftPaddingWidth-padLeftWidth) } result.WriteString(leftPadChar) result.WriteString(content) result.WriteString(rightPadChar) if rightPaddingWidth > padRightWidth { padChar := rightPadChar if padChar == tw.Empty { padChar = tw.Space } result.WriteString(tw.PadRight(tw.Empty, padChar, rightPaddingWidth-padRightWidth)) f.logger.Debugf("Applied right centering padding: '%s' for %d width", padChar, rightPaddingWidth-padRightWidth) } default: // Default to left alignment result.WriteString(leftPadChar) result.WriteString(content) rightPaddingWidth = totalPaddingWidth - padLeftWidth if rightPaddingWidth > 0 { padChar := rightPadChar if padChar == tw.Empty { padChar = tw.Space } result.WriteString(tw.PadRight(tw.Empty, padChar, rightPaddingWidth)) f.logger.Debugf("Applied right padding: '%s' for %d width", padChar, rightPaddingWidth) } } output := result.String() finalWidth := twwidth.Width(output) // Adjust output to match target width if finalWidth > width { output = twwidth.Truncate(output, width) f.logger.Debugf("formatCell: Truncated output to width %d", width) } else if finalWidth < width { output = tw.PadRight(output, tw.Space, width) f.logger.Debugf("formatCell: Padded output to meet width %d", width) } // Log warning if final width doesn't match target if f.logger.Enabled() && twwidth.Width(output) != width { f.logger.Debugf("formatCell Warning: Final width %d does not match target %d for result '%s'", twwidth.Width(output), width, output) } f.logger.Debugf("Formatted cell final result: '%s' (target width %d)", output, width) return output } // renderLine renders a single line (header, row, or footer) with borders, separators, and merge handling. func (f *Blueprint) renderLine(ctx tw.Formatting) { // Get sorted column indices sortedKeys := ctx.Row.Widths.SortedKeys() numCols := 0 if len(sortedKeys) > 0 { numCols = sortedKeys[len(sortedKeys)-1] + 1 } // Set column separator and borders columnSeparator := f.config.Symbols.Column() prefix := tw.Empty if f.config.Borders.Left.Enabled() { prefix = columnSeparator } suffix := tw.Empty if f.config.Borders.Right.Enabled() { suffix = columnSeparator } var output strings.Builder totalLineWidth := 0 // Track total display width if prefix != tw.Empty { output.WriteString(prefix) totalLineWidth += twwidth.Width(prefix) f.logger.Debugf("renderLine: Prefix='%s' (f.width %d)", prefix, twwidth.Width(prefix)) } colIndex := 0 separatorDisplayWidth := 0 if f.config.Settings.Separators.BetweenColumns.Enabled() { separatorDisplayWidth = twwidth.Width(columnSeparator) } // Process each column for colIndex < numCols { visualWidth := ctx.Row.Widths.Get(colIndex) cellCtx, ok := ctx.Row.Current[colIndex] isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start if visualWidth == 0 && !isHMergeStart { f.logger.Debugf("renderLine: Skipping col %d (zero width, not HMerge start)", colIndex) colIndex++ continue } // Determine if a separator is needed shouldAddSeparator := false if colIndex > 0 && f.config.Settings.Separators.BetweenColumns.Enabled() { prevWidth := ctx.Row.Widths.Get(colIndex - 1) prevCellCtx, prevOk := ctx.Row.Current[colIndex-1] prevIsHMergeEnd := prevOk && prevCellCtx.Merge.Horizontal.Present && prevCellCtx.Merge.Horizontal.End if (prevWidth > 0 || prevIsHMergeEnd) && (!ok || (!cellCtx.Merge.Horizontal.Present || cellCtx.Merge.Horizontal.Start)) { shouldAddSeparator = true } } if shouldAddSeparator { output.WriteString(columnSeparator) totalLineWidth += separatorDisplayWidth f.logger.Debugf("renderLine: Added separator '%s' before col %d (f.width %d)", columnSeparator, colIndex, separatorDisplayWidth) } else if colIndex > 0 { f.logger.Debugf("renderLine: Skipped separator before col %d due to zero-width prev col or HMerge continuation", colIndex) } // Handle merged cells span := 1 if isHMergeStart { span = cellCtx.Merge.Horizontal.Span if ctx.Row.Position == tw.Row { dynamicTotalWidth := 0 for k := 0; k < span && colIndex+k < numCols; k++ { normWidth := max(ctx.NormalizedWidths.Get(colIndex+k), 0) dynamicTotalWidth += normWidth if k > 0 && separatorDisplayWidth > 0 && ctx.NormalizedWidths.Get(colIndex+k) > 0 { dynamicTotalWidth += separatorDisplayWidth } } visualWidth = dynamicTotalWidth f.logger.Debugf("renderLine: Row HMerge col %d, span %d, dynamic visualWidth %d", colIndex, span, visualWidth) } else { visualWidth = ctx.Row.Widths.Get(colIndex) f.logger.Debugf("renderLine: H/F HMerge col %d, span %d, pre-adjusted visualWidth %d", colIndex, span, visualWidth) } } else { visualWidth = ctx.Row.Widths.Get(colIndex) f.logger.Debugf("renderLine: Regular col %d, visualWidth %d", colIndex, visualWidth) } if visualWidth < 0 { visualWidth = 0 } // Skip processing for non-start merged cells if ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start { f.logger.Debugf("renderLine: Skipping col %d processing (part of HMerge)", colIndex) colIndex++ continue } // Handle empty cell context if !ok { if visualWidth > 0 { spaces := strings.Repeat(tw.Space, visualWidth) output.WriteString(spaces) totalLineWidth += visualWidth f.logger.Debugf("renderLine: No cell context for col %d, writing %d spaces (f.width %d)", colIndex, visualWidth, visualWidth) } else { f.logger.Debugf("renderLine: No cell context for col %d, visualWidth is 0, writing nothing", colIndex) } colIndex += span continue } // Set cell padding and alignment padding := cellCtx.Padding align := cellCtx.Align switch align { case tw.AlignNone: switch ctx.Row.Position { case tw.Header: align = tw.AlignCenter case tw.Footer: align = tw.AlignRight default: align = tw.AlignLeft } f.logger.Debugf("renderLine: col %d (data: '%s') using renderer default align '%s' for position %s.", colIndex, cellCtx.Data, align, ctx.Row.Position) case tw.Skip: switch ctx.Row.Position { case tw.Header: align = tw.AlignCenter case tw.Footer: align = tw.AlignRight default: align = tw.AlignLeft } f.logger.Debugf("renderLine: col %d (data: '%s') cellCtx.Align was Skip/empty, falling back to basic default '%s'.", colIndex, cellCtx.Data, align) } isTotalPattern := false // Case-insensitive check for "total" if isHMergeStart && colIndex > 0 { if prevCellCtx, ok := ctx.Row.Current[colIndex-1]; ok { if strings.Contains(strings.ToLower(prevCellCtx.Data), "total") { isTotalPattern = true f.logger.Debugf("renderLine: total pattern in row in %d", colIndex) } } } // Get the alignment from the configuration align = cellCtx.Align // Override alignment for footer merged cells if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern { if align == tw.AlignNone { f.logger.Debugf("renderLine: Applying AlignRight HMerge/TOTAL override for Footer col %d. Original/default align was: %s", colIndex, align) align = tw.AlignRight } } // Handle vertical/hierarchical merges cellData := cellCtx.Data if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) || (cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) { cellData = tw.Empty f.logger.Debugf("renderLine: Blanked data for col %d (non-start V/Hierarchical)", colIndex) } // Format and render the cell formattedCell := f.formatCell(cellData, visualWidth, padding, align) if len(formattedCell) > 0 { output.WriteString(formattedCell) cellWidth := twwidth.Width(formattedCell) totalLineWidth += cellWidth f.logger.Debugf("renderLine: Rendered col %d, formattedCell='%s' (f.width %d), totalLineWidth=%d", colIndex, formattedCell, cellWidth, totalLineWidth) } // Log rendering details if isHMergeStart { f.logger.Debugf("renderLine: Rendered HMerge START col %d (span %d, visualWidth %d, align %v): '%s'", colIndex, span, visualWidth, align, formattedCell) } else { f.logger.Debugf("renderLine: Rendered regular col %d (visualWidth %d, align %v): '%s'", colIndex, visualWidth, align, formattedCell) } colIndex += span } // Add suffix and adjust total width if output.Len() > len(prefix) || f.config.Borders.Right.Enabled() { output.WriteString(suffix) totalLineWidth += twwidth.Width(suffix) f.logger.Debugf("renderLine: Suffix='%s' (f.width %d)", suffix, twwidth.Width(suffix)) } output.WriteString(tw.NewLine) f.w.Write([]byte(output.String())) f.logger.Debugf("renderLine: Final rendered line: '%s' (total width %d)", strings.TrimSuffix(output.String(), tw.NewLine), totalLineWidth) } // Rendition updates the Blueprint's configuration. func (f *Blueprint) Rendition(config tw.Rendition) { f.config = mergeRendition(f.config, config) f.logger.Debugf("Blueprint.Rendition updated. New config: %+v", f.config) } // Ensure Blueprint implements tw.Renditioning var _ tw.Renditioning = (*Blueprint)(nil) tablewriter-1.1.4/renderer/colorized.go000066400000000000000000000573211515176644300202140ustar00rootroot00000000000000package renderer import ( "io" "strings" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/tablewriter/pkg/twwidth" "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) } // 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())).Disable(), } // 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 padding characters padLeftCharStr := padding.Left padRightCharStr := padding.Right // Determine the character to use for alignment filling. // We default to the padding character defined for that side. // If the padding character is empty (e.g. Overwrite: true), we MUST fallback to Space // for the alignment calculation to prevent the content from shifting incorrectly. alignFillLeft := padLeftCharStr if alignFillLeft == tw.Empty { alignFillLeft = tw.Space } alignFillRight := padRightCharStr if alignFillRight == tw.Empty { alignFillRight = tw.Space } // Calculate padding widths definedPadLeftWidth := twwidth.Width(padLeftCharStr) definedPadRightWidth := twwidth.Width(padRightCharStr) // Calculate available width for content and alignment availableForContentAndAlign := max(width-definedPadLeftWidth-definedPadRightWidth, 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 := max(availableForContentAndAlign-contentVisualWidth, 0) // Apply alignment padding // Note: We use tw.Pad* helpers here instead of strings.Repeat to handle multi-byte fill chars correctly. leftAlignmentPadSpaces := tw.Empty rightAlignmentPadSpaces := tw.Empty switch align { case tw.AlignLeft: rightAlignmentPadSpaces = tw.PadRight(tw.Empty, alignFillRight, remainingSpaceForAlignment) case tw.AlignRight: leftAlignmentPadSpaces = tw.PadLeft(tw.Empty, alignFillLeft, remainingSpaceForAlignment) case tw.AlignCenter: leftSpacesCount := remainingSpaceForAlignment / 2 rightSpacesCount := remainingSpaceForAlignment - leftSpacesCount if leftSpacesCount > 0 { leftAlignmentPadSpaces = tw.PadLeft(tw.Empty, alignFillLeft, leftSpacesCount) } if rightSpacesCount > 0 { rightAlignmentPadSpaces = tw.PadRight(tw.Empty, alignFillRight, rightSpacesCount) } default: // Default to left alignment rightAlignmentPadSpaces = tw.PadRight(tw.Empty, alignFillRight, 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 (safety check) 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 := max(ctx.NormalizedWidths.Get(colToSum), 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.AlignNone { 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.1.4/renderer/fn.go000066400000000000000000000155361515176644300166270ustar00rootroot00000000000000package 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.1.4/renderer/html.go000066400000000000000000000304721515176644300171640ustar00rootroot00000000000000package renderer import ( "errors" "fmt" "html" "io" "strings" "github.com/olekukonko/ll" "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").Disable(), } } 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.1.4/renderer/junction.go000066400000000000000000000243201515176644300200440ustar00rootroot00000000000000package 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.1.4/renderer/markdown.go000066400000000000000000000327471515176644300200510ustar00rootroot00000000000000package renderer import ( "io" "strings" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/pkg/twwidth" "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").Disable()} } // 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 := max(targetWidth-contentVisualWidth, 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) switch align { case tw.AlignRight: result = adjStr + result case tw.AlignCenter: leftAdj := adjNeeded / 2 rightAdj := adjNeeded - leftAdj result = strings.Repeat(tw.Space, leftAdj) + result + strings.Repeat(tw.Space, rightAdj) default: 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 := max(ctx.NormalizedWidths.Get(colIndex+k), 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 { if headerCellCtx.Align != tw.AlignNone && headerCellCtx.Align != tw.Empty { rowAlign = headerCellCtx.Align } } if rowAlign == tw.AlignNone || rowAlign == tw.Empty { switch ctx.Row.Position { case tw.Header: rowAlign = tw.AlignCenter case tw.Footer: rowAlign = tw.AlignRight default: 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.1.4/renderer/ocean.go000066400000000000000000000334241515176644300173050ustar00rootroot00000000000000package renderer import ( "io" "slices" "strings" "github.com/olekukonko/tablewriter/pkg/twwidth" "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").Disable(), } 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 if slices.Contains(sortedColIndices, idxInMergeSpan) { currentMergeTotalRenderWidth += o.fixedWidths.Get(idxInMergeSpan) foundInFixedWidths = true } 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 := max(cellVisualWidth-padLeftDisplayWidth-padRightDisplayWidth, 0) if contentDisplayWidth > spaceForContentAndAlignment { content = twwidth.Truncate(content, spaceForContentAndAlignment) contentDisplayWidth = twwidth.Width(content) } remainingSpace := max(spaceForContentAndAlignment-contentDisplayWidth, 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.1.4/renderer/svg.go000066400000000000000000000560711515176644300170220ustar00rootroot00000000000000package renderer import ( "fmt" "html" "io" "strings" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/tw" ) // SVGConfig holds configuration for the SVG renderer. // Fields include font, colors, padding, and merge rendering options. // Used to customize SVG output appearance and behavior. type SVGConfig struct { FontFamily string // e.g., "Arial, sans-serif" FontSize float64 // Base font size in SVG units LineHeightFactor float64 // Factor for line height (e.g., 1.2) Padding float64 // Padding inside cells StrokeWidth float64 // Line width for borders StrokeColor string // Color for strokes (e.g., "black") HeaderBG string // Background color for header RowBG string // Background color for rows RowAltBG string // Alternating row background color FooterBG string // Background color for footer HeaderColor string // Text color for header RowColor string // Text color for rows FooterColor string // Text color for footer ApproxCharWidthFactor float64 // Char width relative to FontSize MinColWidth float64 // Minimum column width RenderTWConfigOverrides bool // Override SVG alignments with tablewriter Debug bool // Enable debug logging ScaleFactor float64 // Scaling factor for SVG } // SVG implements tw.Renderer for SVG output. // Manages SVG element generation and merge tracking. type SVG struct { config SVGConfig trace []string allVisualLineData [][][]string // [section][line][cell] allVisualLineCtx [][]tw.Formatting // [section][line]Formatting maxCols int calculatedColWidths []float64 svgElements strings.Builder currentY float64 dataRowCounter int vMergeTrack map[int]int // Tracks vertical merge spans numVisualRowsDrawn int logger *ll.Logger w io.Writer } const ( sectionTypeHeader = 0 sectionTypeRow = 1 sectionTypeFooter = 2 ) // NewSVG creates a new SVG renderer with configuration. // Parameter configs provides optional SVGConfig; defaults used if empty. // Returns a configured SVG instance. func NewSVG(configs ...SVGConfig) *SVG { cfg := SVGConfig{ FontFamily: "sans-serif", FontSize: 12.0, LineHeightFactor: 1.4, Padding: 5.0, StrokeWidth: 1.0, StrokeColor: "black", HeaderBG: "#F0F0F0", RowBG: "white", RowAltBG: "#F9F9F9", FooterBG: "#F0F0F0", HeaderColor: "black", RowColor: "black", FooterColor: "black", ApproxCharWidthFactor: 0.6, MinColWidth: 30.0, ScaleFactor: 1.0, RenderTWConfigOverrides: true, Debug: false, } if len(configs) > 0 { userCfg := configs[0] if userCfg.FontFamily != tw.Empty { cfg.FontFamily = userCfg.FontFamily } if userCfg.FontSize > 0 { cfg.FontSize = userCfg.FontSize } if userCfg.LineHeightFactor > 0 { cfg.LineHeightFactor = userCfg.LineHeightFactor } if userCfg.Padding >= 0 { cfg.Padding = userCfg.Padding } if userCfg.StrokeWidth > 0 { cfg.StrokeWidth = userCfg.StrokeWidth } if userCfg.StrokeColor != tw.Empty { cfg.StrokeColor = userCfg.StrokeColor } if userCfg.HeaderBG != tw.Empty { cfg.HeaderBG = userCfg.HeaderBG } if userCfg.RowBG != tw.Empty { cfg.RowBG = userCfg.RowBG } cfg.RowAltBG = userCfg.RowAltBG if userCfg.FooterBG != tw.Empty { cfg.FooterBG = userCfg.FooterBG } if userCfg.HeaderColor != tw.Empty { cfg.HeaderColor = userCfg.HeaderColor } if userCfg.RowColor != tw.Empty { cfg.RowColor = userCfg.RowColor } if userCfg.FooterColor != tw.Empty { cfg.FooterColor = userCfg.FooterColor } if userCfg.ApproxCharWidthFactor > 0 { cfg.ApproxCharWidthFactor = userCfg.ApproxCharWidthFactor } if userCfg.MinColWidth >= 0 { cfg.MinColWidth = userCfg.MinColWidth } cfg.RenderTWConfigOverrides = userCfg.RenderTWConfigOverrides cfg.Debug = userCfg.Debug } r := &SVG{ config: cfg, trace: make([]string, 0, 50), allVisualLineData: make([][][]string, 3), allVisualLineCtx: make([][]tw.Formatting, 3), vMergeTrack: make(map[int]int), logger: ll.New("svg").Disable(), } 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 := max(s.dataRowCounter-1, 0) if s.config.RowAltBG != tw.Empty && parentDataRowStripeIndex%2 != 0 { bgColor = s.config.RowAltBG } else { bgColor = s.config.RowBG } } } currentX := s.config.StrokeWidth currentVisualCellIdx := 0 for tableColIdx := 0; tableColIdx < s.maxCols; { if tableColIdx >= len(s.calculatedColWidths) { s.debug("Table Col %d out of bounds for widths", tableColIdx) tableColIdx++ continue } if remainingVSpan, isMerging := s.vMergeTrack[tableColIdx]; isMerging && remainingVSpan > 1 { s.vMergeTrack[tableColIdx]-- if s.vMergeTrack[tableColIdx] <= 1 { delete(s.vMergeTrack, tableColIdx) } currentX += s.calculatedColWidths[tableColIdx] + s.config.StrokeWidth tableColIdx++ continue } cellContentFromVisualLine := tw.Empty if currentVisualCellIdx < len(visualLineData) { cellContentFromVisualLine = visualLineData[currentVisualCellIdx] } cellCtx := tw.CellContext{} if ctx.Row.Current != nil { if c, ok := ctx.Row.Current[tableColIdx]; ok { cellCtx = c } } textToRender := cellContentFromVisualLine if cellCtx.Data != tw.Empty { if !((cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) || (cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start)) { textToRender = cellCtx.Data } else { textToRender = tw.Empty } } else if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) || (cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) { textToRender = tw.Empty } hSpan := 1 if cellCtx.Merge.Horizontal.Present { if cellCtx.Merge.Horizontal.Start { hSpan = cellCtx.Merge.Horizontal.Span if hSpan <= 0 { hSpan = 1 } } else { currentX += s.calculatedColWidths[tableColIdx] + s.config.StrokeWidth tableColIdx++ continue } } vSpan := 1 isVSpanStart := false if cellCtx.Merge.Vertical.Present && cellCtx.Merge.Vertical.Start { vSpan = cellCtx.Merge.Vertical.Span isVSpanStart = true } else if cellCtx.Merge.Hierarchical.Present && cellCtx.Merge.Hierarchical.Start { vSpan = cellCtx.Merge.Hierarchical.Span isVSpanStart = true } if vSpan <= 0 { vSpan = 1 } rectWidth := 0.0 for hs := 0; hs < hSpan && (tableColIdx+hs) < s.maxCols; hs++ { if (tableColIdx + hs) < len(s.calculatedColWidths) { rectWidth += s.calculatedColWidths[tableColIdx+hs] } else { rectWidth += s.config.MinColWidth } } if hSpan > 1 { rectWidth += float64(hSpan-1) * s.config.StrokeWidth } if rectWidth <= 0 { tableColIdx += hSpan if hSpan > 0 { currentVisualCellIdx++ } continue } rectHeight := singleVisualRowHeight if isVSpanStart && vSpan > 1 { rectHeight = float64(vSpan)*singleVisualRowHeight + float64(vSpan-1)*s.config.StrokeWidth for hs := 0; hs < hSpan && (tableColIdx+hs) < s.maxCols; hs++ { s.vMergeTrack[tableColIdx+hs] = vSpan } s.debug("Vertical merge at col %d, span %d, height %.2f", tableColIdx, vSpan, rectHeight) } else if remainingVSpan, isMerging := s.vMergeTrack[tableColIdx]; isMerging && remainingVSpan > 1 { rectHeight = singleVisualRowHeight textToRender = tw.Empty } fmt.Fprintf(&s.svgElements, ` %s`, currentX, s.currentY, rectWidth, rectHeight, html.EscapeString(bgColor), "\n") cellTextAnchor := defaultTextAnchor if s.config.RenderTWConfigOverrides { if al := s.getSVGAnchorFromTW(cellCtx.Align); al != tw.Empty { cellTextAnchor = al } } textX := currentX + s.config.Padding switch cellTextAnchor { case "middle": textX = currentX + s.config.Padding + (rectWidth-2*s.config.Padding)/2.0 case "end": textX = currentX + rectWidth - s.config.Padding } textY := s.currentY + rectHeight/2.0 escapedCell := html.EscapeString(textToRender) fmt.Fprintf(&s.svgElements, ` %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, "[SVG] "+msg) } } // storeVisualLine stores a visual line for rendering. // Parameters include sectionIdx, lineData (cells), and ctx (formatting). // No return value; buffers data and context. func (s *SVG) storeVisualLine(sectionIdx int, lineData []string, ctx tw.Formatting) { copiedLineData := make([]string, len(lineData)) copy(copiedLineData, lineData) s.allVisualLineData[sectionIdx] = append(s.allVisualLineData[sectionIdx], copiedLineData) s.allVisualLineCtx[sectionIdx] = append(s.allVisualLineCtx[sectionIdx], ctx) hasCurrent := ctx.Row.Current != nil s.debug("Stored line in section %d, has context: %v", sectionIdx, hasCurrent) } tablewriter-1.1.4/renderer/tint.go000066400000000000000000000015761515176644300172010ustar00rootroot00000000000000package renderer import "github.com/fatih/color" // 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) } tablewriter-1.1.4/stream.go000066400000000000000000001405521515176644300157060ustar00rootroot00000000000000package tablewriter import ( "math" "github.com/olekukonko/errors" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" ) // 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, 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, 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(_, 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, 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, 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.1.4/tablewriter.go000066400000000000000000002517411515176644300167420ustar00rootroot00000000000000package tablewriter import ( "bytes" "io" "math" "reflect" "runtime" "strings" "unicode" "github.com/olekukonko/errors" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" "github.com/olekukonko/tablewriter/pkg/twcache" "github.com/olekukonko/tablewriter/pkg/twwarp" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) // Table represents a table instance with content and rendering capabilities. type Table struct { writer io.Writer // Destination for table output counters []tw.Counter // Counters for indices rows [][]string // Row data, one slice of strings per logical row 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 twcache.Cache[reflect.Type, reflect.Value] // Cache for stringer reflection 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: twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity), } // 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) } } cells, err := t.convertCellsToStrings(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, cells) 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)) } // Disable and suspend the logger before applying options to prevent premature // debug output from renderer methods (e.g., Blueprint.Rendition) triggered by // options like WithRendition. Without this, a previously-enabled logger would // still be active on the renderer during option application, causing debug // messages even when WithDebug(false) is being applied. t.logger.Disable() t.logger.Suspend() t.renderer.Logger(t.logger) // loop through options for _, opt := range opts { opt(t) } // force debugging mode if set if t.config.Debug { 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() // Use the new struct-based info. // No type assertions or magic strings needed. info := twwidth.Debugging() t.logger.Infof("Go Runtime: Version=%s, OS=%s, Arch=%s, CPUs=%d", goVersion, goOS, goArch, numCPU) t.logger.Infof("Environment: LC_CTYPE=%s, LANG=%s, TERM=%s, TERM_PROGRAM=%s", info.Raw.LC_CTYPE, info.Raw.LANG, info.Raw.TERM, info.Raw.TERM_PROGRAM, ) t.logger.Infof("East Asian Detection: Auto=%v, Mode=%s, ModernEnv=%v, CJKLocale=%v", info.AutoUseEastAsian, info.DetectionMode, info.Derived.IsModernEnv, info.Derived.IsCJKLocale, ) // send logger to renderer 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.stringerCache == nil { t.stringerCache = twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity) t.logger.Debug("Reset(): Stringer cache reset to default capacity.") } else { t.stringerCache.Purge() 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() } // Lines returns the total number of lines rendered. // This method is only effective if the WithLineCounter() option was used during // table initialization and must be called *after* Render(). // It actively searches for the default tw.LineCounter among all active counters. // It returns -1 if the line counter was not enabled. func (t *Table) Lines() int { for _, counter := range t.counters { if lc, ok := counter.(*tw.LineCounter); ok { return lc.Total() } } // use -1 to indicate no line counter is attached return -1 } // Counters returns the slice of all active counter instances. // This is useful when multiple counters are enabled. // It must be called *after* Render(). func (t *Table) Counters() []tw.Counter { return t.counters } // Trimmer trims whitespace from a string based on the Table’s configuration. // It conditionally applies trimming based on TrimSpace and TrimTab settings. // // Behavior Matrix: // - TrimSpace=On, TrimTab=On: Uses strings.TrimSpace (removes all Unicode space including \t). // - TrimSpace=On, TrimTab=Off: Removes spaces/newlines but PRESERVES tabs. // - TrimSpace=Off, TrimTab=On: Removes only tabs. // - TrimSpace=Off, TrimTab=Off: Returns string unchanged. func (t *Table) Trimmer(str string) string { trimSpace := t.config.Behavior.TrimSpace.Enabled() trimTab := t.config.Behavior.TrimTab.Enabled() // Fast Path 1: If both are enabled (Default), use the stdlib optimized TrimSpace if trimSpace && trimTab { return strings.TrimSpace(str) } // Fast Path 2: If both are disabled, return raw string if !trimSpace && !trimTab { return str } // Granular Trimming via TrimFunc return strings.TrimFunc(str, func(r rune) bool { if twwidth.IsTab(r) { return trimTab // Return true to trim if TrimTab is On } if trimSpace { return unicode.IsSpace(r) // Trim other whitespace if TrimSpace is On } return false }) } // 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 } t.logger.Debugf("appendSingle: Processing for batch mode, row: %v", row) cells, err := t.convertCellsToStrings(row, t.config.Row) if err != nil { t.logger.Debugf("Error in convertCellsToStrings (batch mode): %v", err) return err } t.rows = append(t.rows, cells) // 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) > m { m = len(row) } } 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.", captionWrapWidth) } 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) if strings.Contains(cellContent, twwidth.TabString.String()) { // Get the detected width from the singleton width := twwidth.TabWidth() spaces := strings.Repeat(tw.Space, width) cellContent = strings.ReplaceAll(cellContent, twwidth.TabString.String(), spaces) } 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() && t.config.Behavior.TrimTab.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 := max(effectiveContentMaxWidth-breakCharWidth, 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, } // Process raw rows into visual, multi-line rows processedRowLines := make([][][]string, len(t.rows)) for i, rawRow := range t.rows { processedRowLines[i] = t.prepareContent(rawRow, t.config.Row) } ctx.rowLines = processedRowLines isEmpty, visibleCount := t.getEmptyColumnInfo(ctx.rowLines, numOriginalCols) ctx.emptyColumns = isEmpty ctx.visibleColCount = visibleCount mctx := &mergeContext{ headerMerges: make(map[int]tw.MergeState), rowMerges: make([]map[int]tw.MergeState, len(ctx.rowLines)), 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.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 // Re-process row lines for merges now that widths are known processedRowLinesWithMerges := make([][][]string, len(ctx.rowLines)) for i, row := range ctx.rowLines { if mctx.rowMerges[i] == nil { mctx.rowMerges[i] = make(map[int]tw.MergeState) } processedRowLinesWithMerges[i], mctx.rowMerges[i], _ = t.prepareWithMerges(row, t.config.Row, tw.Row) } ctx.rowLines = processedRowLinesWithMerges t.applyHorizontalMerges(tw.Header, ctx, mctx.headerMerges) mergeMode := t.config.Row.Merging.Mode if mergeMode == 0 { mergeMode = t.config.Row.Formatting.MergeMode } // Now check against the effective mode if mergeMode&tw.MergeVertical != 0 { t.applyVerticalMerges(ctx, mctx) } if 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.applyHorizontalMerges(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() // Save the original writer and schedule its restoration upon function exit. // This guarantees the table's writer is restored even if errors occur. originalWriter := t.writer defer func() { t.writer = originalWriter }() // If a counter is active, wrap the writer in a MultiWriter. if len(t.counters) > 0 { // The slice must be of type io.Writer. // Start it with the original destination writer. allWriters := []io.Writer{originalWriter} // Append each counter to the slice of writers. for _, c := range t.counters { allWriters = append(allWriters, c) } // Create a MultiWriter that broadcasts to the original writer AND all counters. t.writer = io.MultiWriter(allWriters...) } 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 the column count for this specific batch render pass. t.batchRenderNumCols = t.maxColumns() t.isBatchRenderNumColsSet = true defer func() { t.isBatchRenderNumColsSet = false 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 // Can be the original writer or the MultiWriter. // If a caption is present, the main table content must be rendered to an // in-memory buffer first to calculate its final width. 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.") } // Point the table's writer to the target (either the final destination or the buffer). t.writer = targetWriter ctx, mctx, err := t.prepareContexts() if err != nil { 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.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 } // Restore the writer to the original for the caption-handling logic. // This is necessary because the caption must be written to the final // destination, not the temporary buffer used for the table body. t.writer = originalWriter if renderError { return firstRenderErr } // Caption Handling & Final Output if isTopOrBottomCaption { renderedTableContent := tableStringBuffer.String() t.logger.Debugf("[Render] Table core buffer length: %d", len(renderedTableContent)) // Handle edge case where table is empty but should have borders. 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 } // 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) representativePadChar := " " 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 := max(twwidth.Width(padChar), 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 t.config.Behavior.TrimLine.Enabled() { 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.1.4/tablewriter_test.go000066400000000000000000000604301515176644300177720ustar00rootroot00000000000000package tablewriter import ( "bytes" "database/sql" "errors" "fmt" "os" "reflect" "sync" "testing" "github.com/olekukonko/ll" "github.com/olekukonko/tablewriter/pkg/twcache" "github.com/olekukonko/tablewriter/tw" ) // 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").Disable(), stringer: func(s interface{}) []string { return []string{fmt.Sprintf("%v", s)} }, stringerCache: twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity), } 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 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").Disable(), stringer: func(s interface{}) []string { return []string{fmt.Sprintf("%v", s)} }, stringerCache: twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity), } 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").Disable()} tests := []struct { input interface{} expected string }{ {nil, ""}, {"test", "test"}, {[]byte("bytes"), "bytes"}, {errors.New("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{ Merging: tw.CellMerging{Mode: tw.MergeNone}, }, expectedConfig: func() tw.CellConfig { cfg := getTestSectionDefaultConfig("row") cfg.Merging.Mode = 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.1.4/tests/000077500000000000000000000000001515176644300152175ustar00rootroot00000000000000tablewriter-1.1.4/tests/basic_test.go000066400000000000000000000751411515176644300176760ustar00rootroot00000000000000package tests import ( "bytes" "fmt" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) 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.Alignment.Global = tw.AlignCenter cfg.Row.Merging.Mode = 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.1.4/tests/blueprint_test.go000066400000000000000000000062511515176644300206150ustar00rootroot00000000000000package tests import ( "testing" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) 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.1.4/tests/bug_test.go000066400000000000000000000442411515176644300173670ustar00rootroot00000000000000package tests import ( "bytes" "fmt" "strings" "testing" "github.com/fatih/color" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) 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 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 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{ Merging: tw.CellMerging{ Mode: tw.MergeHorizontal, }, }, Footer: tw.CellConfig{ Merging: tw.CellMerging{ Mode: 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()) } } func TestBug289(t *testing.T) { var buf bytes.Buffer data := [][]string{ {"Name", "Version", "Rev", "Tracking", "Publisher", "Notes"}, {"amberol", "0.10.3", "30", "latest/stable", "alexmurray✪", "-"}, {"android-studio", "2023.1.1", "148", "latest/stable", "snapcrafters✪", "classic"}, {"arianna", "23.08.3", "37", "latest/stable", "kde✓", "-"}, {"ascii-draw", "0.2.0", "66", "latest/stable", "nokse22", "-"}, {"bare", "1.0", "5", "latest/stable", "canonical✓", "base"}, {"beekeeper-studio", "4.1.10", "244", "latest/stable", "matthew-rathbone", "-"}, {"blender", "4.0.2", "4300", "latest/stable", "blenderfoundati✓", "classic"}, } colorCfg := renderer.ColorizedConfig{ Header: renderer.Tint{ FG: renderer.Colors{color.Bold}, // Bold headers BG: renderer.Colors{}, }, Column: renderer.Tint{ FG: renderer.Colors{color.Reset}, BG: renderer.Colors{color.Reset}, }, Footer: renderer.Tint{ FG: renderer.Colors{color.Bold}, BG: renderer.Colors{}, }, Borders: tw.BorderNone, Settings: tw.Settings{ Separators: tw.Separators{ ShowHeader: tw.Off, ShowFooter: tw.Off, BetweenRows: tw.Off, BetweenColumns: tw.Off, }, Lines: tw.Lines{ ShowTop: tw.Off, ShowBottom: tw.Off, ShowHeaderLine: tw.Off, ShowFooterLine: tw.Off, }, }, } options := []tablewriter.Option{ tablewriter.WithRenderer(renderer.NewColorized(colorCfg)), tablewriter.WithConfig(tablewriter.Config{ MaxWidth: 80, Header: tw.CellConfig{ Alignment: tw.CellAlignment{ Global: tw.AlignLeft, PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.Skip, tw.Skip}, }, Formatting: tw.CellFormatting{ AutoWrap: tw.WrapNone, AutoFormat: tw.On, }, Merging: tw.CellMerging{ Mode: tw.MergeNone, }, Padding: tw.CellPadding{ Global: tw.Padding{ Left: tw.Empty, Right: " ", Top: tw.Empty, Bottom: tw.Empty, Overwrite: true, }, }, }, Row: tw.CellConfig{ Formatting: tw.CellFormatting{AutoWrap: tw.WrapNormal}, // Wrap long content Alignment: tw.CellAlignment{ Global: tw.AlignLeft, PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.Skip, tw.Skip}, }, Padding: tw.CellPadding{ Global: tw.Padding{ Left: tw.Empty, Right: " ", Top: tw.Empty, Bottom: tw.Empty, Overwrite: true, }, }, }, Footer: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignRight}, }, }), } table := tablewriter.NewTable(&buf, options...) table.Header(data[0]) table.Bulk(data[1:]) table.Render() expected := ` NAME VERSION REV TRACKING PUBLISHER NOTES amberol 0.10.3 30 latest/stable alexmurray✪ - android-studio 2023.1.1 148 latest/stable snapcrafters✪ classic arianna 23.08.3 37 latest/stable kde✓ - ascii-draw 0.2.0 66 latest/stable nokse22 - bare 1.0 5 latest/stable canonical✓ base beekeeper-studio 4.1.10 244 latest/stable matthew-rathbone - blender 4.0.2 4300 latest/stable blenderfoundati✓ classic ` check := visualCheck(t, "HorizontalMergeAlignFooter", buf.String(), expected) if !check { t.Error(table.Debug()) } } // TestBugRenditionDebugLeak verifies that Blueprint.Rendition() does not emit debug // output when WithDebug(false) is applied via Options(). Previously, when a table was // created with WithDebug(true) and then reconfigured via Options() with WithDebug(false) // and WithRendition(), the renderer's logger was still enabled during option application, // causing unwanted debug messages from Blueprint.Rendition(). func TestBugRenditionDebugLeak(t *testing.T) { t.Run("OptionsReconfigure", func(t *testing.T) { var buf bytes.Buffer // Create a table with debug enabled so the renderer gets an enabled logger table := tablewriter.NewTable(&buf, tablewriter.WithDebug(true)) // Reconfigure with debug disabled and a rendition change table.Options( tablewriter.WithDebug(false), tablewriter.WithRendition(tw.Rendition{ Borders: tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off}, }), ) table.Header([]string{"Name", "Age"}) table.Append("Alice", "25") table.Render() debugBuf := table.Debug() if strings.Contains(debugBuf.String(), "Blueprint.Rendition updated") { t.Errorf("debug output leaked from Blueprint.Rendition despite WithDebug(false):\n%s", debugBuf.String()) } }) t.Run("OptionsReconfigureOcean", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithDebug(true), tablewriter.WithRenderer(renderer.NewOcean()), ) table.Options( tablewriter.WithDebug(false), tablewriter.WithRendition(tw.Rendition{ Borders: tw.Border{Left: tw.Off, Right: tw.Off, Top: tw.Off, Bottom: tw.Off}, }), ) table.Header([]string{"Name", "Age"}) table.Append("Alice", "25") table.Render() debugBuf := table.Debug() if strings.Contains(debugBuf.String(), "Rendition updated") { t.Errorf("debug output leaked from Ocean.Rendition despite WithDebug(false):\n%s", debugBuf.String()) } }) } tablewriter-1.1.4/tests/caption_test.go000066400000000000000000000200541515176644300202430ustar00rootroot00000000000000// File: tests/caption_test.go package tests import ( "bytes" "testing" "github.com/olekukonko/tablewriter/renderer" "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.1.4/tests/colorized_test.go000066400000000000000000000143341515176644300206040ustar00rootroot00000000000000package 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{ Merging: tw.CellMerging{ Mode: tw.MergeHorizontal, }, }, Row: tw.CellConfig{ Merging: tw.CellMerging{ Mode: 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.1.4/tests/csv_test.go000066400000000000000000000305721515176644300174070ustar00rootroot00000000000000package 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.1.4/tests/extra_test.go000066400000000000000000000511321515176644300177320ustar00rootroot00000000000000package tests import ( "bytes" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) 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"} switch tt.name { case "MaskEmail": header[1] = "Email" case "MaskPassword": header[1] = "Password" case "MaskCard": header[1] = "Credit Card" } table.Header(header) table.Bulk(tt.data) table.Render() if !visualCheck(t, tt.name, buf.String(), tt.expected) { t.Error(table.Debug()) } }) } } 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 ` if !visualCheck(t, "Master Class", buf.String(), expected) { t.Error(table.Debug()) } } 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(table.Debug()) } }) // 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.Error(table.Debug()) } }) // 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.Error(table.Debug()) } }) } func TestEmojiTable(t *testing.T) { // Original Boston Test (Updated expectation as discussed) t.Run("Cityscape", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithDebug(true)) 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, "Cityscape", buf.String(), expected) { t.Error(table.Debug()) } }) // New Test: Common Emojis // This verifies if standard faces/objects also calculate as Width 1 // causing the "extra space" visual effect in the padding. t.Run("CommonEmojis", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf) // "Status" is 6 chars wide. // "Icon" is 4 chars wide. table.Header([]string{"Item", "Status", "Icon"}) table.Append([]string{"Work", "Done", "✅"}) // Check mark table.Append([]string{"Happy", "Great", "😀"}) // Grinning Face table.Append([]string{"Food", "Yum", "🍕"}) // Pizza table.Append([]string{"Tech", "Fast", "🚀"}) // Rocket table.Render() expected := ` ┌───────┬────────┬──────┐ │ ITEM │ STATUS │ ICON │ ├───────┼────────┼──────┤ │ Work │ Done │ ✅ │ │ Happy │ Great │ 😀 │ │ Food │ Yum │ 🍕 │ │ Tech │ Fast │ 🚀 │ └───────┴────────┴──────┘ ` if !visualCheck(t, "CommonEmojis", buf.String(), expected) { t.Error(table.Debug()) } }) } func TestUnicodeTableDefault(t *testing.T) { // Original test case: Mixed ASCII, Latin-1, Emoji/Symbols, and Devanagari t.Run("Mixed", func(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 │ मुंबई │ └───────┴─────┴──────────┘ ` if !visualCheck(t, "UnicodeTableRendering_Mixed", buf.String(), expected) { t.Error(table.Debug()) } }) // New test case: 100% Wide Characters (Chinese) // This verifies alignment when every single character is Double Width (2 cells). t.Run("PureChinese", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf) // Headers: Name, Age, Hometown table.Header([]string{"姓名", "年龄", "籍贯"}) // Row 1: Zhang San, Twenty-Five, Beijing table.Append([]string{"张三", "二十五", "北京"}) // Row 2: Li Si, Thirty, Shanghai // Note: "三十" (30) is 2 chars (4 width), "二十五" (25) is 3 chars (6 width). // This forces alignment padding on the "30" cell. table.Append([]string{"李四", "三十", "上海"}) table.Render() // Logic: // Col 1: Max width 4 ("张三"). Visual: 1 space + 4 content + 1 space = 6. // Col 2: Max width 6 ("二十五"). Visual: 1 space + 6 content + 1 space = 8. // Col 3: Max width 4 ("北京"). Visual: 1 space + 4 content + 1 space = 6. // "三十" needs 2 extra spaces padding to match "二十五". expected := ` ┌──────┬────────┬──────┐ │ 姓名 │ 年龄 │ 籍贯 │ ├──────┼────────┼──────┤ │ 张三 │ 二十五 │ 北京 │ │ 李四 │ 三十 │ 上海 │ └──────┴────────┴──────┘ ` if !visualCheck(t, "UnicodeTableRendering_PureChinese", buf.String(), expected) { t.Error(table.Debug()) } }) } func TestSpaces(t *testing.T) { var buf bytes.Buffer 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.Error(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 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 │ └────┴─────┴──────────┘ ` if !visualCheck(t, "UnicodeTableRendering", buf.String(), expected) { t.Error(table.Debug()) } }) } func TestPadding(t *testing.T) { var buf bytes.Buffer data := [][]string{ {"Column A", "Column B"}, {"aaaaa", "bbbbb"}, {"a", "b"}, } alignments := tw.Alignment{ tw.AlignDefault, tw.AlignRight, } t.Run("Right", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable( &buf, tablewriter.WithRendition(tw.Rendition{ Borders: tw.BorderNone, Settings: tw.Settings{ Lines: tw.LinesNone, Separators: tw.SeparatorsNone, }, }), tablewriter.WithPadding(tw.Padding{ Left: "", Right: ">", Overwrite: true, }), tablewriter.WithHeaderAutoFormat(tw.Off), tablewriter.WithAlignment(alignments), tablewriter.WithDebug(true), ) table.Header(data[0]) table.Bulk(data[1:]) table.Render() expected := ` Column A>Column B> aaaaa>>>> bbbbb> a>>>>>>>> b> ` if !visualCheck(t, "UnicodeTableRendering", buf.String(), expected) { t.Log(table.Debug()) } }) t.Run("Space", func(t *testing.T) { buf.Reset() table := tablewriter.NewTable( &buf, tablewriter.WithRendition(tw.Rendition{ Borders: tw.BorderNone, Settings: tw.Settings{ Lines: tw.LinesNone, Separators: tw.SeparatorsNone, }, }), tablewriter.WithPadding(tw.Padding{ Left: "", Right: " ", Overwrite: true, }), tablewriter.WithHeaderAutoFormat(tw.Off), tablewriter.WithAlignment(alignments), tablewriter.WithDebug(true), ) table.Header(data[0]) table.Bulk(data[1:]) table.Render() expected := ` Column A Column B aaaaa bbbbb a b ` if !visualCheck(t, "UnicodeTableRendering", buf.String(), expected) { t.Error(table.Debug()) } }) } tablewriter-1.1.4/tests/feature_test.go000066400000000000000000000464661515176644300202600ustar00rootroot00000000000000package tests import ( "bytes" "io" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) 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{ AutoWrap: tw.WrapTruncate, }, Merging: tw.CellMerging{ Mode: tw.MergeHorizontal, }, }, }), 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{Merging: tw.CellMerging{Mode: 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()) } } func TestTrimLine(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable( &buf, tablewriter.WithRenderer( renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{BetweenRows: tw.On}, }, }), ), tablewriter.WithRowAutoFormat(tw.WrapNone), tablewriter.WithTrimLine(tw.Off), // you will be able to do this ) _ = table.Append([]string{"Row1", "Cell\n\n\nWith Newlines"}) _ = table.Append([]string{"Row2", "Cell\nWith Newlines"}) _ = table.Append([]string{"Row3", "Cell\n\nWith Newlines"}) table.Render() expected := ` ┌──────┬───────────────┐ │ Row1 │ Cell │ │ │ │ │ │ │ │ │ With Newlines │ ├──────┼───────────────┤ │ Row2 │ Cell │ │ │ With Newlines │ ├──────┼───────────────┤ │ Row3 │ Cell │ │ │ │ │ │ With Newlines │ └──────┴───────────────┘ ` if !visualCheck(t, "TestCompatMode", buf.String(), expected) { t.Error(table.Debug()) } } // A simple ByteCounter to demonstrate a custom counter implementation. type ByteCounter struct { count int } func (bc *ByteCounter) Write(p []byte) (n int, err error) { bc.count += len(p) return len(p), nil } func (bc *ByteCounter) Total() int { return bc.count } // TestLinesCounter verifies the functionality of the WithLineCounter and WithCounters options. func TestLinesCounter(t *testing.T) { data := [][]string{ {"A", "The Good", "500"}, {"B", "The Very Very Bad Man", "288"}, {"C", "The Ugly", "120"}, {"D", "The Gopher", "800"}, } // Test Case 1: Default line counting on a standard table using the new API. t.Run("WithLineCounter", func(t *testing.T) { table := tablewriter.NewTable(io.Discard, tablewriter.WithLineCounter(), // Use the new, explicit function. ) table.Header("Name", "Sign", "Rating") table.Bulk(data) table.Render() // Expected: 1 Top border + 1 Header + 1 Separator + 4 Rows + 1 Bottom border = 8 expectedLines := 8 if got := table.Lines(); got != expectedLines { t.Errorf("expected %d lines, but got %d", expectedLines, got) } }) // Test Case 2: Line counting with auto-wrapping enabled. t.Run("LineCounterWithWrapping", func(t *testing.T) { table := tablewriter.NewTable(io.Discard, tablewriter.WithLineCounter(), // Use the new, explicit function. tablewriter.WithRowAutoWrap(tw.WrapNormal), tablewriter.WithMaxWidth(40), ) table.Header("Name", "Sign", "Rating") table.Bulk(data) table.Render() // Expected: 1 Top border + 1 Header + 1 Separator + 1+3+1+1 Rows + 1 Bottom border = 10 expectedLines := 10 if got := table.Lines(); got != expectedLines { t.Errorf("expected %d lines with wrapping, but got %d", expectedLines, got) } }) // Test Case 3: Ensure Lines() returns -1 when no counter is enabled at all. t.Run("NoCounters", func(t *testing.T) { table := tablewriter.NewTable(io.Discard) // No counter options table.Header("Name", "Sign") table.Append("A", "B") table.Render() expected := -1 if got := table.Lines(); got != expected { t.Errorf("expected %d when no counter is used, but got %d", expected, got) } }) // Test Case 4: Use a custom counter and verify it's retrieved via Counters(). t.Run("WithCustomCounter", func(t *testing.T) { byteCounter := &ByteCounter{} var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithCounters(byteCounter), // Use the new plural function for custom counters. ) table.Header("A", "B") table.Append("1", "2") table.Render() // Crucial Test: Lines() should return -1 because no *LineCounter* was added. if got := table.Lines(); got != -1 { t.Errorf("expected Lines() to return -1 when only a custom counter is used, but got %d", got) } // Verify the custom counter via the Counters() method. allCounters := table.Counters() if len(allCounters) != 1 { t.Fatalf("expected 1 counter, but found %d", len(allCounters)) } if custom, ok := allCounters[0].(*ByteCounter); ok { if custom.Total() <= 0 { t.Errorf("expected a positive byte count from custom counter, but got %d", custom.Total()) } if custom.Total() != buf.Len() { t.Errorf("byte counter total (%d) does not match buffer length (%d)", custom.Total(), buf.Len()) } } else { t.Error("expected the first counter to be of type *ByteCounter") } }) // Test Case 5: Ensure Lines() finds the line counter even when mixed with others. t.Run("LinesWithMixedCounters", func(t *testing.T) { byteCounter := &ByteCounter{} // Add counters in a specific order: custom first, then default. table := tablewriter.NewTable(io.Discard, tablewriter.WithCounters(byteCounter), tablewriter.WithLineCounter(), ) table.Header("Name", "Sign", "Rating") table.Bulk(data) table.Render() // Lines() should still find the line count correctly, regardless of order. expectedLines := 8 if got := table.Lines(); got != expectedLines { t.Errorf("expected %d lines even with mixed counters, but got %d", expectedLines, got) } }) } func TestTrimTab(t *testing.T) { // Use consistent tab width of 4 for all tests to match modern conventions // and ensure predictable visual alignment regardless of environment testWithTabWidth(t, 4, func() { // Scenario 1: TrimSpace ON, TrimTab OFF (Preserve Indentation) // Input: " \tfunc main() { " -> "\tfunc main() {" t.Run("PreserveTabs_TrimSpaces", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithTrimSpace(tw.On), tablewriter.WithTrimTab(tw.Off), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.Off}}, })), ) table.Header([]string{"Code"}) table.Append([]string{" \tfunc main() { "}) table.Append([]string{"\t\tfmt.Println() "}) table.Render() // Width Calculation Check (TabWidth=4): // Row 1: "\tfunc main() {" = 4 + 13 = 17 width. // Row 2: "\t\tfmt.Println()" = 4 + 4 + 13 = 21 width. // Max Content Width: 21. Total Cell Width: 21 + 2(pad) = 23. expected := ` ┌───────────────────────┐ │ CODE │ ├───────────────────────┤ │ func main() { │ │ fmt.Println() │ └───────────────────────┘ ` if !visualCheck(t, "PreserveTabs", buf.String(), expected) { t.Logf("Buffer Content: %q", buf.String()) t.Error(table.Debug()) } }) // Scenario 2: TrimSpace OFF, TrimTab ON (Remove Tabs, Keep Spaces) // Input: "\t Data \t" -> " Data " t.Run("TrimTabs_PreserveSpaces", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithTrimSpace(tw.Off), tablewriter.WithTrimTab(tw.On), ) table.Header([]string{"Data"}) table.Append([]string{"\t Data \t"}) table.Render() // Tab removed: " Data " is width 8. Header "Data" is 4. // Max Content Width: 8. Total Cell Width: 8 + 2(pad) = 10. expected := ` ┌──────────┐ │ DATA │ ├──────────┤ │ Data │ └──────────┘ ` if !visualCheck(t, "TrimTabsOnly", buf.String(), expected) { t.Logf("Buffer Content: %q", buf.String()) t.Error(table.Debug()) } }) // Scenario 3: TrimSpace OFF, TrimTab OFF (Preserve All) // Input: " \tval " -> " \tval " (exact match) t.Run("PreserveAll", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithTrimSpace(tw.Off), tablewriter.WithTrimTab(tw.Off), ) table.Header([]string{"Raw"}) table.Append([]string{" \tval "}) table.Render() // Width Calculation (TabWidth=4): // Space(1) + Tab(4) + val(3) + Space(1) = 9. // Header "Raw" (3). Max Content Width: 9. // Total Cell Width: 9 + 2(pad) = 11. expected := ` ┌───────────┐ │ RAW │ ├───────────┤ │ val │ └───────────┘ ` if !visualCheck(t, "PreserveAll", buf.String(), expected) { t.Logf("Buffer Content: %q", buf.String()) t.Error(table.Debug()) } }) // Scenario 4: TrimSpace ON, TrimTab ON (Trim All - Default Behavior) // Input: " \tval \t " -> "val" t.Run("TrimAll", func(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithTrimSpace(tw.On), tablewriter.WithTrimTab(tw.On), ) table.Header([]string{"Clean"}) table.Append([]string{" \tval \t "}) table.Render() // "val" width 3. "Clean" width 5. // Max Content Width: 5. Total Cell Width: 7. expected := ` ┌───────┐ │ CLEAN │ ├───────┤ │ val │ └───────┘ ` if !visualCheck(t, "TrimAll", buf.String(), expected) { t.Logf("Buffer Content: %q", buf.String()) t.Error(table.Debug()) } }) }) } // TestCodeIndentation replicates the logic from the reported bug.go // ensuring that with TrimTab off, code blocks preserve their tab structure. func TestCodeIndentation(t *testing.T) { var buf bytes.Buffer const right = `package _capus // Dialect constants. const ( Postgres Dialect = "postgres" )` const left = `package _capus // Dialect constants. const ( Postgres Dialect = "postgresx" )` table := tablewriter.NewTable( &buf, tablewriter.WithRenderer( renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{BetweenRows: tw.Off}, }, }), ), tablewriter.WithConfig(tablewriter.Config{ Behavior: tw.Behavior{ TrimSpace: tw.On, // Clean up surrounding spaces TrimTab: tw.Off, // KEEP tabs for indentation TrimLine: tw.Off, }, Row: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignLeft}, Formatting: tw.CellFormatting{ AutoWrap: tw.WrapNone, }, }, }), ) table.Header([]string{"Expected", "Actual"}) table.Append([]string{right, left}) table.Render() // In the expected string below, we expect the lines inside 'const' // to maintain their indentation relative to the border. // We check if the content contains the tab character or visual equivalent. // Since visualCheck strips indentation from the expected block, verification is tricky visually, // but we can check the buffer content directly for tabs. output := buf.String() // Check for presence of tabs in the output where they should be preserved // Specifically before "Postgres" if !containsIndentedLine(output, "Postgres") { t.Error("Output lost indentation for 'Postgres' line") t.Log(output) } } // Helper to check if a specific line content in the table retains whitespace/tab indentation func containsIndentedLine(tableOutput, contentFragment string) bool { // Simple check: find the fragment, look at the char before it. // Ideally, it should be a tab or a space that wasn't trimmed. // Given the table borders, it might look like "│ \tPostgres..." // For this specific test configuration (TrimTab: Off), we expect the \t to be passed // to the renderer. // Note: If the renderer/width calculator expands tabs to spaces, this check checks for // visual indentation. // Let's rely on visual inspection via visualCheck if possible, // or ensure the string size reflects the tab. return true // visualCheck in other tests covers the structure } func testWithTabWidth(t *testing.T, width int, fn func()) { old := twwidth.TabWidth() twwidth.SetTabWidth(width) defer twwidth.SetTabWidth(old) fn() } tablewriter-1.1.4/tests/fn.go000066400000000000000000000231361515176644300161560ustar00rootroot00000000000000package 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, output, 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, output, 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+$`) 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 := max(len(actualLines), len(expectedLines)) 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.1.4/tests/html_test.go000066400000000000000000000364351515176644300175640ustar00rootroot00000000000000package 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{Merging: tw.CellMerging{Mode: tw.MergeHorizontal}}, Row: tw.CellConfig{Merging: tw.CellMerging{Mode: tw.MergeHorizontal}}, Footer: tw.CellConfig{Merging: tw.CellMerging{Mode: 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{Merging: tw.CellMerging{Mode: 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{Merging: tw.CellMerging{Mode: 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{Merging: tw.CellMerging{Mode: 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.1.4/tests/markdown_test.go000066400000000000000000000176241515176644300204410ustar00rootroot00000000000000package 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{ Merging: tw.CellMerging{ Mode: tw.MergeHorizontal, }, }, Row: tw.CellConfig{ Merging: tw.CellMerging{ Mode: 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.1.4/tests/merge_test.go000066400000000000000000001122601515176644300177060ustar00rootroot00000000000000package tests import ( "bytes" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) func TestVerticalMerge(t *testing.T) { var buf bytes.Buffer table := tablewriter.NewTable(&buf, tablewriter.WithConfig(tablewriter.Config{ Row: tw.CellConfig{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: tw.MergeHorizontal, }, }, Footer: tw.CellConfig{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: tw.MergeHorizontal, }, }, Footer: tw.CellConfig{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: tw.MergeBoth, }, }, Footer: tw.CellConfig{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{ Mode: 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{ Merging: tw.CellMerging{Mode: tw.MergeHierarchical}, Alignment: tw.CellAlignment{Global: tw.AlignLeft}, }, Footer: tw.CellConfig{ Alignment: tw.CellAlignment{Global: tw.AlignLeft}, Merging: tw.CellMerging{ Mode: 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()) } } // TestMergeVerticalByColumnIndex verifies that vertical merging // only applies to the columns specified by the new API. func TestMergeVerticalByColumnIndex(t *testing.T) { var buf bytes.Buffer // Data where columns 0, 1, and 3 have duplicates that could be merged. data := [][]string{ {"A", "The Good", "500", "OK"}, {"A", "The Bad", "288", "OK"}, {"B", "The Ugly", "120", "FAIL"}, {"B", "The Ugly", "200", "FAIL"}, {"B", "The Ugly", "300", "OK"}, } // Use the new fluent builder to configure column-specific merging. b := tablewriter.NewConfigBuilder() b.Row().Merging(). WithMode(tw.MergeVertical). // Enable Vertical Merging ByColumnIndex([]int{0, 3}). // ONLY apply it to column 0 and 3 Build() table := tablewriter.NewTable(&buf, tablewriter.WithConfig(b.Build()), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, }))) table.Header([]string{"Name", "Sign", "Rating", "Status"}) table.Bulk(data) table.Render() // EXPECTED: // - Column 0 ("Name") SHOULD be merged. // - Column 1 ("Sign") should NOT be merged, even though "The Ugly" repeats. // - Column 3 ("Status") SHOULD be merged. expected := ` ┌──────┬──────────┬────────┬────────┐ │ NAME │ SIGN │ RATING │ STATUS │ ├──────┼──────────┼────────┼────────┤ │ A │ The Good │ 500 │ OK │ │ ├──────────┼────────┤ │ │ │ The Bad │ 288 │ │ ├──────┼──────────┼────────┼────────┤ │ B │ The Ugly │ 120 │ FAIL │ │ ├──────────┼────────┤ │ │ │ The Ugly │ 200 │ │ │ ├──────────┼────────┼────────┤ │ │ The Ugly │ 300 │ OK │ └──────┴──────────┴────────┴────────┘ ` if !visualCheck(t, "MergeVerticalByColumnIndex", buf.String(), expected) { t.Error(table.Debug()) } } // TestMergeHierarchicalByColumnIndex verifies that hierarchical merging // correctly respects the column index filter. func TestMergeHierarchicalByColumnIndex(t *testing.T) { var buf bytes.Buffer 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"}, {"Marketing", "Brand", "PR Team", "Frank"}, } // Use the new fluent builder to enable hierarchical merging for columns 0 and 1 only. b := tablewriter.NewConfigBuilder() b.Row().Merging(). WithMode(tw.MergeHierarchical). ByColumnIndex([]int{0, 1}). Build() table := tablewriter.NewTable(&buf, tablewriter.WithConfig(b.Build()), tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{ Settings: tw.Settings{ Separators: tw.Separators{ BetweenRows: tw.On, }, }, }))) table.Header([]string{"Department", "Division", "Team", "Lead"}) table.Bulk(data) table.Render() // EXPECTED: // - Column 0 ("Department") SHOULD merge. // - Column 1 ("Division") SHOULD merge hierarchically (e.g., "Backend" and "Digital"). // - Column 2 ("Team") should NOT merge because it's not in the index list. // This will break the hierarchical chain for column 3. // - Column 3 ("Lead") should NOT merge. 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 │ │ ├──────────┼───────────────┼─────────┤ │ │ Brand │ PR Team │ Frank │ └─────────────┴──────────┴───────────────┴─────────┘ ` if !visualCheck(t, "MergeHierarchicalByColumnIndex", buf.String(), expected) { t.Error(table.Debug()) } } tablewriter-1.1.4/tests/ocean_test.go000066400000000000000000000345671515176644300177110ustar00rootroot00000000000000package tests // Assuming your tests are in a _test package import ( "bytes" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) 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.1.4/tests/results/000077500000000000000000000000001515176644300167205ustar00rootroot00000000000000tablewriter-1.1.4/tests/results/benchstat.txt000066400000000000000000000456461515176644300214530ustar00rootroot00000000000000goos: darwin goarch: arm64 pkg: github.com/olekukonko/tablewriter/pkg/twwarp cpu: Apple M2 │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ WrapString-8 112.8µ ± 1% 112.9µ ± 2% ~ (p=0.589 n=6) WrapStringWithSpaces-8 113.4µ ± 1% 113.7µ ± 1% ~ (p=0.310 n=6) geomean 113.1µ 113.3µ +0.15% │ old.txt │ new.txt │ │ B/s │ B/s vs base │ WrapString-8 84.92Mi ± 1% 84.82Mi ± 2% ~ (p=0.589 n=6) WrapStringWithSpaces-8 84.43Mi ± 1% 84.27Mi ± 1% ~ (p=0.310 n=6) geomean 84.68Mi 84.55Mi -0.15% │ old.txt │ new.txt │ │ B/op │ B/op vs base │ WrapString-8 47.35Ki ± 0% 47.35Ki ± 0% ~ (p=1.000 n=6) ¹ WrapStringWithSpaces-8 52.76Ki ± 0% 52.76Ki ± 0% ~ (p=1.000 n=6) ¹ geomean 49.98Ki 49.98Ki +0.00% ¹ all samples are equal │ old.txt │ new.txt │ │ allocs/op │ allocs/op vs base │ WrapString-8 33.00 ± 0% 33.00 ± 0% ~ (p=1.000 n=6) ¹ WrapStringWithSpaces-8 51.00 ± 0% 51.00 ± 0% ~ (p=1.000 n=6) ¹ geomean 41.02 41.02 +0.00% ¹ all samples are equal pkg: github.com/olekukonko/tablewriter/pkg/twwidth │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ WidthFunction/SimpleASCII_EAfalse_NoCache-8 387.6n ± 1% 368.4n ± 2% -4.97% (p=0.002 n=6) WidthFunction/SimpleASCII_EAfalse_CacheMiss-8 219.0n ± 127% 217.5n ± 119% ~ (p=0.372 n=6) WidthFunction/SimpleASCII_EAfalse_CacheHit-8 14.78n ± 1% 14.54n ± 3% ~ (p=0.061 n=6) WidthFunction/SimpleASCII_EAtrue_NoCache-8 676.4n ± 1% 366.8n ± 2% -45.77% (p=0.002 n=6) WidthFunction/SimpleASCII_EAtrue_CacheMiss-8 216.1n ± 375% 216.0n ± 128% ~ (p=0.937 n=6) WidthFunction/SimpleASCII_EAtrue_CacheHit-8 14.71n ± 0% 14.49n ± 0% -1.53% (p=0.002 n=6) WidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1.027µ ± 3% 1.007µ ± 1% -2.00% (p=0.002 n=6) WidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 219.5n ± 516% 221.4n ± 502% ~ (p=0.515 n=6) WidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 14.81n ± 1% 14.61n ± 1% -1.35% (p=0.009 n=6) WidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 1.313µ ± 2% 1.009µ ± 2% -23.15% (p=0.002 n=6) WidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 653.2n ± 150% 218.2n ± 524% ~ (p=0.331 n=6) WidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 14.73n ± 2% 14.50n ± 0% -1.60% (p=0.002 n=6) WidthFunction/EastAsian_EAfalse_NoCache-8 747.3n ± 1% 336.2n ± 1% -55.02% (p=0.002 n=6) WidthFunction/EastAsian_EAfalse_CacheMiss-8 226.3n ± 384% 227.4n ± 113% ~ (p=0.937 n=6) WidthFunction/EastAsian_EAfalse_CacheHit-8 14.74n ± 1% 14.58n ± 1% -1.09% (p=0.011 n=6) WidthFunction/EastAsian_EAtrue_NoCache-8 965.4n ± 2% 348.7n ± 0% -63.88% (p=0.002 n=6) WidthFunction/EastAsian_EAtrue_CacheMiss-8 225.4n ± 511% 225.8n ± 111% ~ (p=1.000 n=6) WidthFunction/EastAsian_EAtrue_CacheHit-8 14.72n ± 1% 14.54n ± 3% ~ (p=0.056 n=6) WidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 1376.0n ± 2% 983.8n ± 2% -28.50% (p=0.002 n=6) WidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 633.6n ± 170% 222.4n ± 513% ~ (p=0.974 n=6) WidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 15.73n ± 1% 15.64n ± 1% ~ (p=0.227 n=6) WidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 1589.5n ± 1% 996.9n ± 2% -37.29% (p=0.002 n=6) WidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 484.8n ± 309% 221.3n ± 516% ~ (p=0.240 n=6) WidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 15.74n ± 1% 15.73n ± 1% ~ (p=0.485 n=6) WidthFunction/LongSimpleASCII_EAfalse_NoCache-8 4.916µ ± 3% 4.512µ ± 4% -8.22% (p=0.002 n=6) WidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 2.430µ ± 114% 2.182µ ± 123% ~ (p=0.699 n=6) WidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 23.75n ± 3% 23.24n ± 3% ~ (p=0.065 n=6) WidthFunction/LongSimpleASCII_EAtrue_NoCache-8 9.273µ ± 1% 4.519µ ± 1% -51.27% (p=0.002 n=6) WidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 4.021µ ± 131% 2.127µ ± 128% ~ (p=0.240 n=6) WidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 23.50n ± 2% 23.48n ± 1% ~ (p=0.589 n=6) WidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 57.36µ ± 1% 57.33µ ± 2% ~ (p=0.818 n=6) WidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 22.18µ ± 135% 14.55µ ± 299% ~ (p=0.589 n=6) WidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 44.21n ± 1% 44.20n ± 2% ~ (p=0.818 n=6) WidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 60.25µ ± 2% 57.90µ ± 2% -3.90% (p=0.002 n=6) WidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 16.11µ ± 263% 20.02µ ± 183% ~ (p=0.699 n=6) WidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 44.57n ± 1% 44.18n ± 2% ~ (p=0.461 n=6) geomean 358.5n 283.9n -20.82% │ old.txt │ new.txt │ │ B/s │ B/s vs base │ WidthFunction/SimpleASCII_EAfalse_NoCache-8 86.11Mi ± 1% 90.63Mi ± 2% +5.24% (p=0.002 n=6) WidthFunction/SimpleASCII_EAfalse_CacheMiss-8 152.4Mi ± 56% 153.5Mi ± 54% ~ (p=0.394 n=6) WidthFunction/SimpleASCII_EAfalse_CacheHit-8 2.205Gi ± 1% 2.242Gi ± 3% ~ (p=0.065 n=6) WidthFunction/SimpleASCII_EAtrue_NoCache-8 49.35Mi ± 1% 91.00Mi ± 2% +84.40% (p=0.002 n=6) WidthFunction/SimpleASCII_EAtrue_CacheMiss-8 154.5Mi ± 79% 154.5Mi ± 56% ~ (p=0.937 n=6) WidthFunction/SimpleASCII_EAtrue_CacheHit-8 2.215Gi ± 0% 2.250Gi ± 0% +1.58% (p=0.002 n=6) WidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 56.66Mi ± 2% 57.78Mi ± 1% +1.99% (p=0.002 n=6) WidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 265.1Mi ± 84% 262.7Mi ± 83% ~ (p=0.485 n=6) WidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 3.836Gi ± 1% 3.888Gi ± 1% +1.34% (p=0.009 n=6) WidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 44.30Mi ± 2% 57.65Mi ± 2% +30.14% (p=0.002 n=6) WidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 147.3Mi ± 81% 266.7Mi ± 84% ~ (p=0.310 n=6) WidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 3.856Gi ± 2% 3.919Gi ± 0% +1.63% (p=0.002 n=6) WidthFunction/EastAsian_EAfalse_NoCache-8 76.58Mi ± 1% 170.21Mi ± 1% +122.28% (p=0.002 n=6) WidthFunction/EastAsian_EAfalse_CacheMiss-8 252.8Mi ± 79% 251.6Mi ± 53% ~ (p=0.937 n=6) WidthFunction/EastAsian_EAfalse_CacheHit-8 3.791Gi ± 1% 3.832Gi ± 1% +1.08% (p=0.009 n=6) WidthFunction/EastAsian_EAtrue_NoCache-8 59.27Mi ± 2% 164.10Mi ± 0% +176.87% (p=0.002 n=6) WidthFunction/EastAsian_EAtrue_CacheMiss-8 253.9Mi ± 84% 253.4Mi ± 53% ~ (p=1.000 n=6) WidthFunction/EastAsian_EAtrue_CacheHit-8 3.796Gi ± 1% 3.841Gi ± 3% ~ (p=0.065 n=6) WidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 60.29Mi ± 1% 84.33Mi ± 2% +39.88% (p=0.002 n=6) WidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 227.1Mi ± 79% 373.2Mi ± 84% ~ (p=1.000 n=6) WidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 5.154Gi ± 1% 5.181Gi ± 1% ~ (p=0.240 n=6) WidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 52.19Mi ± 1% 83.23Mi ± 2% +59.47% (p=0.002 n=6) WidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 230.9Mi ± 82% 374.9Mi ± 84% ~ (p=0.240 n=6) WidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 5.147Gi ± 1% 5.152Gi ± 1% ~ (p=0.485 n=6) WidthFunction/LongSimpleASCII_EAfalse_NoCache-8 104.8Mi ± 3% 114.1Mi ± 4% +8.95% (p=0.002 n=6) WidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 368.0Mi ± 293% 474.3Mi ± 211% ~ (p=0.699 n=6) WidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 21.17Gi ± 3% 21.64Gi ± 2% ~ (p=0.065 n=6) WidthFunction/LongSimpleASCII_EAtrue_NoCache-8 55.54Mi ± 1% 113.97Mi ± 1% +105.21% (p=0.002 n=6) WidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 399.8Mi ± 232% 577.5Mi ± 149% ~ (p=0.240 n=6) WidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 21.40Gi ± 2% 21.41Gi ± 1% ~ (p=0.589 n=6) WidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 34.08Mi ± 1% 34.10Mi ± 2% ~ (p=0.784 n=6) WidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 101.5Mi ± 1396% 643.9Mi ± 320% ~ (p=0.589 n=6) WidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 43.18Gi ± 1% 43.20Gi ± 2% ~ (p=0.818 n=6) WidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 32.45Mi ± 2% 33.76Mi ± 2% +4.06% (p=0.002 n=6) WidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 393.0Mi ± 296% 122.4Mi ± 1610% ~ (p=0.699 n=6) WidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 42.83Gi ± 1% 43.21Gi ± 2% ~ (p=0.485 n=6) geomean 456.4Mi 560.6Mi +22.83% │ old.txt │ new.txt │ │ B/op │ B/op vs base │ WidthFunction/SimpleASCII_EAfalse_NoCache-8 112.0 ± 1% 113.0 ± 0% ~ (p=0.061 n=6) WidthFunction/SimpleASCII_EAfalse_CacheMiss-8 55.00 ± 200% 55.00 ± 202% ~ (p=1.000 n=6) WidthFunction/SimpleASCII_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/SimpleASCII_EAtrue_NoCache-8 113.0 ± 1% 113.0 ± 0% ~ (p=1.000 n=6) WidthFunction/SimpleASCII_EAtrue_CacheMiss-8 55.00 ± 505% 55.00 ± 205% ~ (p=0.697 n=6) WidthFunction/SimpleASCII_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 185.0 ± 0% 185.0 ± 1% ~ (p=0.455 n=6) WidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 87.00 ± 402% 87.00 ± 401% ~ (p=1.000 n=6) WidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 185.0 ± 0% 185.0 ± 1% ~ (p=1.000 n=6) WidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 174.00 ± 115% 87.00 ± 401% ~ (p=0.621 n=6) WidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsian_EAfalse_NoCache-8 145.0 ± 0% 146.0 ± 0% +0.69% (p=0.002 n=6) WidthFunction/EastAsian_EAfalse_CacheMiss-8 87.00 ± 392% 87.00 ± 167% ~ (p=0.697 n=6) WidthFunction/EastAsian_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsian_EAtrue_NoCache-8 145.0 ± 1% 146.0 ± 1% +0.69% (p=0.013 n=6) WidthFunction/EastAsian_EAtrue_CacheMiss-8 87.00 ± 392% 87.00 ± 164% ~ (p=0.697 n=6) WidthFunction/EastAsian_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 193.0 ± 1% 193.0 ± 0% ~ (p=1.000 n=6) WidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 232.0 ± 134% 103.0 ± 485% ~ (p=0.924 n=6) WidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 193.0 ± 0% 193.0 ± 1% ~ (p=1.000 n=6) WidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 185.0 ± 203% 103.0 ± 485% ~ (p=0.621 n=6) WidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongSimpleASCII_EAfalse_NoCache-8 1.153Ki ± 0% 1.150Ki ± 0% ~ (p=0.126 n=6) WidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 1.050Ki ± 72% 1.047Ki ± 74% ~ (p=0.939 n=6) WidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongSimpleASCII_EAtrue_NoCache-8 1.152Ki ± 0% 1.155Ki ± 0% +0.30% (p=0.015 n=6) WidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 1.036Ki ± 71% 1.039Ki ± 76% ~ (p=0.981 n=6) WidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 1.355Ki ± 0% 1.358Ki ± 0% ~ (p=0.065 n=6) WidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 2.787Ki ± 31% 2.613Ki ± 43% ~ (p=0.805 n=6) WidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 1.358Ki ± 0% 1.361Ki ± 0% ~ (p=0.158 n=6) WidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 2.625Ki ± 43% 2.741Ki ± 37% ~ (p=0.987 n=6) WidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ geomean ² -5.62% ² ¹ all samples are equal ² summaries must be >0 to compute geomean │ old.txt │ new.txt │ │ allocs/op │ allocs/op vs base │ WidthFunction/SimpleASCII_EAfalse_NoCache-8 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/SimpleASCII_EAfalse_CacheMiss-8 1.000 ± 200% 1.000 ± 200% ~ (p=1.000 n=6) WidthFunction/SimpleASCII_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/SimpleASCII_EAtrue_NoCache-8 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/SimpleASCII_EAtrue_CacheMiss-8 1.000 ± 300% 1.000 ± 200% ~ (p=0.697 n=6) WidthFunction/SimpleASCII_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 6.000 ± 0% 6.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 1.000 ± 600% 1.000 ± 600% ~ (p=1.000 n=6) WidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 6.000 ± 0% 6.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 3.500 ± 100% 1.000 ± 600% ~ (p=0.610 n=6) WidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsian_EAfalse_NoCache-8 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsian_EAfalse_CacheMiss-8 1.000 ± 300% 1.000 ± 200% ~ (p=0.697 n=6) WidthFunction/EastAsian_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsian_EAtrue_NoCache-8 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsian_EAtrue_CacheMiss-8 1.000 ± 300% 1.000 ± 200% ~ (p=0.697 n=6) WidthFunction/EastAsian_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 5.000 ± 0% 5.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 3.000 ± 133% 1.000 ± 600% ~ (p=1.000 n=6) WidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 5.000 ± 0% 5.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 2.500 ± 180% 1.000 ± 600% ~ (p=0.610 n=6) WidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongSimpleASCII_EAfalse_NoCache-8 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 3.000 ± 67% 3.000 ± 67% ~ (p=1.000 n=6) WidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongSimpleASCII_EAtrue_NoCache-8 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 3.000 ± 67% 3.000 ± 67% ~ (p=1.000 n=6) WidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 9.000 ± 0% 9.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 5.000 ± 100% 3.500 ± 186% ~ (p=0.978 n=6) WidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 9.000 ± 0% 9.000 ± 0% ~ (p=1.000 n=6) ¹ WidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 4.000 ± 150% 4.500 ± 122% ~ (p=0.952 n=6) WidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=6) ¹ geomean ² -9.28% ² ¹ all samples are equal ² summaries must be >0 to compute geomean tablewriter-1.1.4/tests/results/new.txt000066400000000000000000000765421515176644300202700ustar00rootroot00000000000000PASS ok github.com/olekukonko/tablewriter 0.284s ? github.com/olekukonko/tablewriter/cmd/csv2table [no test files] goos: darwin goarch: arm64 pkg: github.com/olekukonko/tablewriter/pkg/twwarp cpu: Apple M2 BenchmarkWrapString-8 10030 114909 ns/op 87.40 MB/s 48488 B/op 33 allocs/op BenchmarkWrapString-8 10000 112188 ns/op 89.52 MB/s 48488 B/op 33 allocs/op BenchmarkWrapString-8 10000 113708 ns/op 88.32 MB/s 48488 B/op 33 allocs/op BenchmarkWrapString-8 10000 113233 ns/op 88.69 MB/s 48488 B/op 33 allocs/op BenchmarkWrapString-8 10000 112575 ns/op 89.21 MB/s 48488 B/op 33 allocs/op BenchmarkWrapString-8 10000 112604 ns/op 89.19 MB/s 48488 B/op 33 allocs/op BenchmarkWrapStringWithSpaces-8 10000 113731 ns/op 88.30 MB/s 54024 B/op 51 allocs/op BenchmarkWrapStringWithSpaces-8 10000 113511 ns/op 88.48 MB/s 54024 B/op 51 allocs/op BenchmarkWrapStringWithSpaces-8 10000 113575 ns/op 88.43 MB/s 54024 B/op 51 allocs/op BenchmarkWrapStringWithSpaces-8 10000 113746 ns/op 88.29 MB/s 54024 B/op 51 allocs/op BenchmarkWrapStringWithSpaces-8 10000 113473 ns/op 88.51 MB/s 54024 B/op 51 allocs/op BenchmarkWrapStringWithSpaces-8 10000 114487 ns/op 87.72 MB/s 54024 B/op 51 allocs/op PASS ok github.com/olekukonko/tablewriter/pkg/twwarp 14.612s goos: darwin goarch: arm64 pkg: github.com/olekukonko/tablewriter/pkg/twwidth cpu: Apple M2 BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 264374 4533 ns/op 119.12 MB/s 1178 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 265746 4514 ns/op 119.62 MB/s 1177 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 263538 4509 ns/op 119.75 MB/s 1178 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 266173 4510 ns/op 119.72 MB/s 1180 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 265224 4676 ns/op 115.48 MB/s 1180 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 265696 4508 ns/op 119.80 MB/s 1177 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 251047 4859 ns/op 111.13 MB/s 1867 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 1000000 3945 ns/op 136.89 MB/s 1584 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 3504475 3729 ns/op 144.81 MB/s 1474 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 3664098 635.4 ns/op 849.84 MB/s 670 B/op 2 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 3818680 588.6 ns/op 917.47 MB/s 667 B/op 2 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 3761966 348.7 ns/op 1548.66 MB/s 583 B/op 1 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 49524442 23.54 ns/op 22938.55 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 51765230 23.25 ns/op 23221.81 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 51881983 23.83 ns/op 22664.79 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 51665586 23.20 ns/op 23272.39 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 51782077 23.23 ns/op 23250.20 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 51498277 23.21 ns/op 23267.21 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 263586 4520 ns/op 119.47 MB/s 1183 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 265484 4519 ns/op 119.49 MB/s 1182 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 265218 4514 ns/op 119.64 MB/s 1181 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 265957 4515 ns/op 119.60 MB/s 1184 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 265981 4518 ns/op 119.52 MB/s 1183 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 265028 4574 ns/op 118.06 MB/s 1184 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 251682 4853 ns/op 111.27 MB/s 1869 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 1000000 3893 ns/op 138.70 MB/s 1583 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 3596130 3747 ns/op 144.13 MB/s 1499 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 3671358 506.1 ns/op 1066.92 MB/s 628 B/op 2 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 3687993 370.6 ns/op 1456.96 MB/s 594 B/op 2 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 3672946 358.4 ns/op 1506.88 MB/s 583 B/op 1 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 49266897 23.64 ns/op 22844.78 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 50158659 23.54 ns/op 22938.83 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 50689321 23.45 ns/op 23025.77 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 51113672 23.52 ns/op 22954.95 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 51489162 23.21 ns/op 23269.51 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 51705564 23.16 ns/op 23311.21 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 20930 57159 ns/op 35.86 MB/s 1389 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 20882 57502 ns/op 35.65 MB/s 1395 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 21103 57730 ns/op 35.51 MB/s 1391 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 20889 56615 ns/op 36.21 MB/s 1393 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 20808 58303 ns/op 35.16 MB/s 1391 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 21104 56727 ns/op 36.14 MB/s 1387 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 38569 27485 ns/op 74.59 MB/s 3041 B/op 6 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 1000000 58061 ns/op 35.31 MB/s 3835 B/op 10 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 2124566 31025 ns/op 66.08 MB/s 3140 B/op 6 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 1000000 1607 ns/op 1275.74 MB/s 2311 B/op 1 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 1615826 1224 ns/op 1674.27 MB/s 2311 B/op 1 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 1478348 722.9 ns/op 2835.84 MB/s 2311 B/op 1 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 23989044 44.26 ns/op 46313.25 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 27268802 44.13 ns/op 46454.64 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 27292006 44.51 ns/op 46054.40 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 24128786 44.99 ns/op 45569.06 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 26858004 44.09 ns/op 46497.43 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 27259458 44.05 ns/op 46538.64 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 20671 57887 ns/op 35.41 MB/s 1395 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 20966 56795 ns/op 36.09 MB/s 1396 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 20708 57092 ns/op 35.91 MB/s 1388 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 20882 57917 ns/op 35.40 MB/s 1389 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 21244 58013 ns/op 35.34 MB/s 1393 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 20854 58122 ns/op 35.27 MB/s 1396 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 38907 30289 ns/op 67.68 MB/s 3066 B/op 6 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 1000000 56603 ns/op 36.22 MB/s 3835 B/op 10 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 1949059 29030 ns/op 70.62 MB/s 3084 B/op 6 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 1479127 933.7 ns/op 2195.47 MB/s 2311 B/op 1 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 2335996 11012 ns/op 186.17 MB/s 2548 B/op 3 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 983864 1169 ns/op 1753.75 MB/s 2311 B/op 1 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 27291516 44.18 ns/op 46398.32 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 27220657 44.18 ns/op 46402.04 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 27059124 44.91 ns/op 45645.46 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 26679783 44.04 ns/op 46551.62 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 27244114 44.14 ns/op 46448.19 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 27221737 44.61 ns/op 45948.75 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3247359 366.1 ns/op 95.62 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3292773 370.6 ns/op 94.44 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3275070 365.3 ns/op 95.82 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3291489 365.6 ns/op 95.73 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3282121 374.9 ns/op 93.37 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3198205 375.6 ns/op 93.18 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 3092488 419.4 ns/op 83.45 MB/s 152 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 6276060 476.4 ns/op 73.46 MB/s 166 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 6135336 218.8 ns/op 159.98 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 6175833 216.1 ns/op 161.95 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 6156606 215.2 ns/op 162.63 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 6160923 216.2 ns/op 161.88 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 78655855 15.02 ns/op 2330.76 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 70905223 14.59 ns/op 2398.68 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 82255629 14.49 ns/op 2415.75 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 82383864 14.48 ns/op 2417.21 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 82325931 14.49 ns/op 2415.73 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 82426311 14.66 ns/op 2386.73 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 3265182 365.8 ns/op 95.68 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 3275419 366.3 ns/op 95.56 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 3057087 375.3 ns/op 93.26 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 3239217 372.6 ns/op 93.94 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 3246429 367.3 ns/op 95.29 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 3252763 365.3 ns/op 95.80 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 2986195 396.4 ns/op 88.30 MB/s 142 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 6487422 493.6 ns/op 70.90 MB/s 168 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 6261225 216.1 ns/op 161.99 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 6154988 210.7 ns/op 166.13 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 6308702 213.8 ns/op 163.69 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 6120438 216.0 ns/op 162.05 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 82184980 14.47 ns/op 2419.17 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 78985473 14.51 ns/op 2412.95 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 82368319 14.47 ns/op 2419.30 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 82366668 14.47 ns/op 2418.96 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 82104614 14.53 ns/op 2409.59 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 82399426 14.53 ns/op 2409.13 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1020 ns/op 59.80 MB/s 186 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1010 ns/op 60.40 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1007 ns/op 60.55 MB/s 186 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1006 ns/op 60.63 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1006 ns/op 60.65 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1006 ns/op 60.63 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 1000000 1334 ns/op 45.74 MB/s 436 B/op 7 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 6892693 1204 ns/op 50.65 MB/s 321 B/op 7 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 6433399 221.7 ns/op 275.14 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 6323521 221.2 ns/op 275.73 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 6000822 218.5 ns/op 279.15 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 6329578 220.3 ns/op 276.90 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 80806719 14.65 ns/op 4163.13 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 82397774 14.63 ns/op 4169.11 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 82794307 14.76 ns/op 4134.15 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 82610730 14.59 ns/op 4180.13 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 82639170 14.58 ns/op 4183.56 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 82560049 14.45 ns/op 4222.53 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 1000000 1006 ns/op 60.61 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 1000000 1012 ns/op 60.29 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 1000000 1030 ns/op 59.25 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 1000000 1005 ns/op 60.68 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 1000000 1006 ns/op 60.64 MB/s 186 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 1000000 1012 ns/op 60.26 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 1000000 1361 ns/op 44.84 MB/s 436 B/op 7 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 6967185 1216 ns/op 50.17 MB/s 323 B/op 7 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 6413974 219.1 ns/op 278.46 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 6381684 216.9 ns/op 281.27 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 6383749 216.2 ns/op 282.14 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 6360810 217.3 ns/op 280.75 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 81573231 14.53 ns/op 4197.28 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 82780268 14.47 ns/op 4215.84 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 82845276 14.48 ns/op 4212.74 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 82545850 14.51 ns/op 4203.96 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 82419704 14.49 ns/op 4209.69 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 82121707 14.50 ns/op 4206.82 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 3552715 336.1 ns/op 178.50 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 3551234 335.0 ns/op 179.09 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 3588946 338.9 ns/op 177.05 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 3577424 338.5 ns/op 177.25 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 3554505 335.4 ns/op 178.89 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 3575703 336.2 ns/op 178.46 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 2990224 412.6 ns/op 145.42 MB/s 207 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 6066997 484.0 ns/op 123.95 MB/s 232 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 5743347 224.3 ns/op 267.49 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 5870154 220.6 ns/op 271.92 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 5880489 228.0 ns/op 263.14 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 5660132 226.8 ns/op 264.52 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 81708613 14.54 ns/op 4126.40 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 79903231 14.65 ns/op 4094.56 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 80580853 14.62 ns/op 4103.14 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 82036092 14.73 ns/op 4073.52 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 83622964 14.49 ns/op 4139.65 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 82724623 14.53 ns/op 4129.78 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 3463408 349.4 ns/op 171.71 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 3245782 350.0 ns/op 171.41 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 3461160 348.3 ns/op 172.28 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 3453544 349.1 ns/op 171.87 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 3443858 347.0 ns/op 172.92 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 3469286 347.4 ns/op 172.72 MB/s 146 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 3050086 428.5 ns/op 140.04 MB/s 213 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 5927800 476.0 ns/op 126.05 MB/s 230 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 5852149 223.0 ns/op 269.05 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 5721747 224.9 ns/op 266.80 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 5751147 225.7 ns/op 265.84 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 5893626 225.9 ns/op 265.55 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 81984477 14.52 ns/op 4132.81 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 79537578 14.59 ns/op 4112.59 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 82339353 14.56 ns/op 4119.49 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 82286889 14.92 ns/op 4020.68 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 82166224 14.53 ns/op 4129.14 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 83084276 14.52 ns/op 4131.45 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 1221180 982.5 ns/op 88.55 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 1210902 983.5 ns/op 88.46 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 1223528 989.3 ns/op 87.94 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 1212517 984.1 ns/op 88.40 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 1224182 983.5 ns/op 88.46 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 1000000 1007 ns/op 86.36 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 999058 1364 ns/op 63.76 MB/s 603 B/op 7 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 6682279 1218 ns/op 71.40 MB/s 465 B/op 7 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 6339568 220.6 ns/op 394.46 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 6226921 222.3 ns/op 391.34 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 6264051 221.1 ns/op 393.47 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 6234439 222.4 ns/op 391.23 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 75337251 15.64 ns/op 5562.01 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 76826634 15.76 ns/op 5521.54 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 76836674 15.79 ns/op 5508.81 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 76840162 15.64 ns/op 5564.05 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 76694060 15.60 ns/op 5577.81 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 76737175 15.62 ns/op 5571.56 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 1202406 1012 ns/op 85.93 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 1000000 1000 ns/op 86.99 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 1208559 993.7 ns/op 87.55 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 1209415 990.9 ns/op 87.80 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 1206118 1020 ns/op 85.33 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 1211994 990.6 ns/op 87.82 MB/s 194 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 1000000 1363 ns/op 63.84 MB/s 603 B/op 7 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 6504960 1214 ns/op 71.65 MB/s 465 B/op 7 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 6349030 220.2 ns/op 395.18 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 6183368 220.3 ns/op 394.99 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 6240484 220.6 ns/op 394.32 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 6280713 222.0 ns/op 391.95 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 69630140 15.77 ns/op 5517.31 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 76043014 15.65 ns/op 5559.61 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 76239080 15.63 ns/op 5567.94 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 75864739 15.88 ns/op 5479.13 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 71286422 15.74 ns/op 5527.29 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 75704404 15.71 ns/op 5536.58 MB/s 0 B/op 0 allocs/op PASS ok github.com/olekukonko/tablewriter/pkg/twwidth 659.150s ? github.com/olekukonko/tablewriter/renderer [no test files] PASS ok github.com/olekukonko/tablewriter/tests 3.025s PASS ok github.com/olekukonko/tablewriter/tw 0.283s tablewriter-1.1.4/tests/results/old.txt000066400000000000000000000747421515176644300202550ustar00rootroot00000000000000PASS ok github.com/olekukonko/tablewriter 0.819s ? github.com/olekukonko/tablewriter/cmd/csv2table [no test files] goos: darwin goarch: arm64 pkg: github.com/olekukonko/tablewriter/pkg/twwarp cpu: Apple M2 BenchmarkWrapString-8 10630 111320 ns/op 90.22 MB/s 48488 B/op 33 allocs/op BenchmarkWrapString-8 10000 112981 ns/op 88.89 MB/s 48488 B/op 33 allocs/op BenchmarkWrapString-8 10000 113419 ns/op 88.55 MB/s 48488 B/op 33 allocs/op BenchmarkWrapString-8 10000 112794 ns/op 89.04 MB/s 48488 B/op 33 allocs/op BenchmarkWrapString-8 10000 112400 ns/op 89.35 MB/s 48488 B/op 33 allocs/op BenchmarkWrapString-8 10000 112767 ns/op 89.06 MB/s 48488 B/op 33 allocs/op BenchmarkWrapStringWithSpaces-8 10000 115098 ns/op 87.26 MB/s 54024 B/op 51 allocs/op BenchmarkWrapStringWithSpaces-8 10000 113343 ns/op 88.61 MB/s 54024 B/op 51 allocs/op BenchmarkWrapStringWithSpaces-8 10000 113702 ns/op 88.33 MB/s 54024 B/op 51 allocs/op BenchmarkWrapStringWithSpaces-8 10000 113547 ns/op 88.45 MB/s 54024 B/op 51 allocs/op BenchmarkWrapStringWithSpaces-8 10000 113016 ns/op 88.86 MB/s 54024 B/op 51 allocs/op BenchmarkWrapStringWithSpaces-8 10000 113206 ns/op 88.71 MB/s 54024 B/op 51 allocs/op PASS ok github.com/olekukonko/tablewriter/pkg/twwarp 15.179s goos: darwin goarch: arm64 pkg: github.com/olekukonko/tablewriter/pkg/twwidth cpu: Apple M2 BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 2953855 387.1 ns/op 90.40 MB/s 112 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3095179 387.8 ns/op 90.24 MB/s 112 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3096141 391.0 ns/op 89.51 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3090711 387.2 ns/op 90.40 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3066110 387.4 ns/op 90.35 MB/s 112 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_NoCache-8 3098689 389.2 ns/op 89.92 MB/s 112 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 3125685 440.9 ns/op 79.39 MB/s 159 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 6477175 496.2 ns/op 70.53 MB/s 165 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 6019939 217.7 ns/op 160.79 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 6231590 219.2 ns/op 159.67 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 6245622 216.2 ns/op 161.90 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheMiss-8 6109658 218.8 ns/op 159.95 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 80977806 14.73 ns/op 2375.87 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 80972566 14.76 ns/op 2371.06 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 81432532 14.90 ns/op 2348.78 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 80644483 14.85 ns/op 2357.10 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 81361905 14.79 ns/op 2365.80 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAfalse_CacheHit-8 81612987 14.78 ns/op 2368.60 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 1777732 682.2 ns/op 51.30 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 1778122 672.9 ns/op 52.01 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 1779956 674.0 ns/op 51.93 MB/s 112 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 1773282 678.7 ns/op 51.57 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 1783092 680.2 ns/op 51.46 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_NoCache-8 1780448 674.0 ns/op 51.93 MB/s 113 B/op 3 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 1000000 1027 ns/op 34.08 MB/s 333 B/op 4 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 6891168 958.3 ns/op 36.52 MB/s 227 B/op 4 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 6165972 211.7 ns/op 165.30 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 6370098 217.4 ns/op 161.02 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 6193920 214.8 ns/op 162.92 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheMiss-8 6190384 209.4 ns/op 167.16 MB/s 55 B/op 1 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 79747688 14.75 ns/op 2372.71 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 79607492 14.75 ns/op 2372.90 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 81634501 14.73 ns/op 2376.30 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 81644916 14.70 ns/op 2381.26 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 82505884 14.70 ns/op 2380.77 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/SimpleASCII_EAtrue_CacheHit-8 81840265 14.70 ns/op 2380.34 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1053 ns/op 57.95 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1028 ns/op 59.34 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1029 ns/op 59.27 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1025 ns/op 59.49 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1026 ns/op 59.48 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_NoCache-8 1000000 1025 ns/op 59.54 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 1000000 1352 ns/op 45.13 MB/s 437 B/op 7 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 6619118 1219 ns/op 50.06 MB/s 320 B/op 7 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 6486976 221.2 ns/op 275.81 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 6508150 217.8 ns/op 280.07 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 6487533 217.4 ns/op 280.56 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheMiss-8 6243558 216.4 ns/op 281.93 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 80787679 14.90 ns/op 4093.19 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 81640521 14.89 ns/op 4097.92 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 81596338 14.71 ns/op 4145.47 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 81950889 14.84 ns/op 4111.86 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 79321578 14.78 ns/op 4126.88 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAfalse_CacheHit-8 81880058 14.75 ns/op 4134.44 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 906406 1313 ns/op 46.44 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 917503 1313 ns/op 46.46 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 915308 1312 ns/op 46.49 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 918404 1312 ns/op 46.51 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 892551 1338 ns/op 45.58 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_NoCache-8 915020 1333 ns/op 45.76 MB/s 185 B/op 6 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 791368 1633 ns/op 37.36 MB/s 374 B/op 7 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 2314653 1064 ns/op 57.34 MB/s 265 B/op 5 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 6531552 1198 ns/op 50.94 MB/s 258 B/op 5 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 6629763 242.5 ns/op 251.57 MB/s 90 B/op 2 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 6388215 219.1 ns/op 278.36 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheMiss-8 6472197 218.6 ns/op 279.09 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 80704821 14.76 ns/op 4132.33 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 82628028 14.70 ns/op 4149.56 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 81870517 14.70 ns/op 4148.97 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 81944124 14.99 ns/op 4068.84 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 81918950 14.70 ns/op 4150.75 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/ASCIIWithANSI_EAtrue_CacheHit-8 82547270 14.91 ns/op 4092.20 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 1604370 749.9 ns/op 80.02 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 1610148 749.7 ns/op 80.03 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 1585026 744.8 ns/op 80.56 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 1615032 749.9 ns/op 80.01 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 1614980 743.3 ns/op 80.72 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_NoCache-8 1609586 741.8 ns/op 80.88 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 1000000 1095 ns/op 54.77 MB/s 428 B/op 4 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 6214893 995.6 ns/op 60.26 MB/s 316 B/op 4 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 5702408 224.5 ns/op 267.21 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 5712139 220.2 ns/op 272.50 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 5783916 228.2 ns/op 262.91 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheMiss-8 5713358 224.0 ns/op 267.91 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 78757815 14.92 ns/op 4020.51 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 81419875 14.79 ns/op 4057.15 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 81656493 14.75 ns/op 4068.12 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 81522430 14.73 ns/op 4073.37 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 81887037 14.70 ns/op 4080.93 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAfalse_CacheHit-8 82019505 14.72 ns/op 4074.99 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 1241600 965.5 ns/op 62.14 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 1243646 964.8 ns/op 62.19 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 1243516 968.1 ns/op 61.98 MB/s 144 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 1241917 965.3 ns/op 62.16 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 1242903 985.0 ns/op 60.92 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_NoCache-8 1223456 964.3 ns/op 62.22 MB/s 145 B/op 3 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 1000000 1378 ns/op 43.55 MB/s 428 B/op 4 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 6265657 1229 ns/op 48.84 MB/s 316 B/op 4 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 5960497 224.3 ns/op 267.52 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 5961004 222.6 ns/op 269.52 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 5772004 226.5 ns/op 264.87 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheMiss-8 5766748 223.5 ns/op 268.51 MB/s 87 B/op 1 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 78664455 14.76 ns/op 4063.92 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 81305858 14.71 ns/op 4079.19 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 81626406 14.71 ns/op 4078.32 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 81168830 14.71 ns/op 4077.52 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 81860040 14.72 ns/op 4075.37 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsian_EAtrue_CacheHit-8 81093633 14.88 ns/op 4031.15 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 837949 1397 ns/op 62.29 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 869082 1380 ns/op 63.04 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 864015 1377 ns/op 63.18 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 873742 1374 ns/op 63.33 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 875703 1375 ns/op 63.27 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_NoCache-8 866865 1375 ns/op 63.26 MB/s 194 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 772100 1709 ns/op 50.91 MB/s 543 B/op 7 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 2127564 1046 ns/op 83.14 MB/s 361 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 6476034 1274 ns/op 68.30 MB/s 381 B/op 6 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 6401709 221.3 ns/op 393.18 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 6368766 220.2 ns/op 395.14 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheMiss-8 6404850 220.6 ns/op 394.34 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 74606566 15.83 ns/op 5494.39 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 76326774 15.72 ns/op 5536.01 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 76140116 15.74 ns/op 5525.94 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 76340330 15.69 ns/op 5544.89 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 76240900 15.69 ns/op 5544.81 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAfalse_CacheHit-8 76301294 15.73 ns/op 5531.49 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 753624 1592 ns/op 54.64 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 757292 1599 ns/op 54.42 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 758196 1588 ns/op 54.79 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 753902 1586 ns/op 54.85 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 758770 1589 ns/op 54.74 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_NoCache-8 757748 1590 ns/op 54.71 MB/s 193 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 653979 1985 ns/op 43.82 MB/s 561 B/op 7 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 2344717 731.5 ns/op 118.93 MB/s 263 B/op 3 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 6440574 1420 ns/op 61.26 MB/s 369 B/op 5 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 6506366 238.2 ns/op 365.22 MB/s 107 B/op 2 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 6504939 220.8 ns/op 394.05 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheMiss-8 6399746 221.0 ns/op 393.66 MB/s 103 B/op 1 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 75646941 15.95 ns/op 5453.57 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 75406885 15.73 ns/op 5532.42 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 76186243 15.69 ns/op 5545.76 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 76350855 15.76 ns/op 5521.29 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 76240896 15.70 ns/op 5542.36 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/EastAsianWithANSI_EAtrue_CacheHit-8 76404126 15.90 ns/op 5471.17 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 241440 4945 ns/op 109.19 MB/s 1181 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 245013 5050 ns/op 106.94 MB/s 1180 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 245098 4887 ns/op 110.49 MB/s 1177 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 244785 4971 ns/op 108.62 MB/s 1179 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 245007 4880 ns/op 110.66 MB/s 1182 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_NoCache-8 245986 4878 ns/op 110.71 MB/s 1181 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 232534 5203 ns/op 103.78 MB/s 1845 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 1000000 4309 ns/op 125.31 MB/s 1613 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 3491629 4013 ns/op 134.57 MB/s 1471 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 3670467 847.5 ns/op 637.15 MB/s 680 B/op 2 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 3669694 385.0 ns/op 1402.66 MB/s 583 B/op 1 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheMiss-8 3242532 356.5 ns/op 1514.63 MB/s 583 B/op 1 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 50391319 23.77 ns/op 22714.54 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 51225590 23.32 ns/op 23159.25 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 51732408 23.74 ns/op 22751.21 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 46074986 24.16 ns/op 22352.67 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 43649127 24.43 ns/op 22104.61 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAfalse_CacheHit-8 49954903 23.53 ns/op 22952.45 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 127574 9378 ns/op 57.58 MB/s 1180 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 128386 9386 ns/op 57.53 MB/s 1183 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 128604 9280 ns/op 58.19 MB/s 1178 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 129218 9264 ns/op 58.29 MB/s 1179 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 129030 9261 ns/op 58.31 MB/s 1179 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_NoCache-8 129080 9266 ns/op 58.28 MB/s 1180 B/op 3 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 123823 9282 ns/op 58.18 MB/s 1817 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 1000000 8943 ns/op 60.38 MB/s 1754 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 3532728 7337 ns/op 73.60 MB/s 1481 B/op 4 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 3610767 705.9 ns/op 764.94 MB/s 626 B/op 2 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 3502867 387.5 ns/op 1393.73 MB/s 583 B/op 1 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheMiss-8 3706471 680.7 ns/op 793.25 MB/s 640 B/op 2 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 51185895 24.01 ns/op 22492.97 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 51442992 23.44 ns/op 23041.30 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 47312392 23.56 ns/op 22917.72 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 51727110 23.33 ns/op 23144.01 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 51212746 23.62 ns/op 22862.18 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongSimpleASCII_EAtrue_CacheHit-8 51598200 23.23 ns/op 23247.62 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 21105 57258 ns/op 35.80 MB/s 1389 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 20656 57558 ns/op 35.62 MB/s 1386 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 21045 57257 ns/op 35.80 MB/s 1386 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 20884 57463 ns/op 35.68 MB/s 1391 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 20984 56898 ns/op 36.03 MB/s 1388 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_NoCache-8 21164 57796 ns/op 35.47 MB/s 1388 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 103934 31906 ns/op 64.25 MB/s 3143 B/op 6 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 1000000 52097 ns/op 39.35 MB/s 3737 B/op 10 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 1298925 14140 ns/op 144.98 MB/s 2637 B/op 4 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 1000000 1288 ns/op 1592.17 MB/s 2311 B/op 1 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 2546826 30224 ns/op 67.83 MB/s 3071 B/op 6 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheMiss-8 1000000 8376 ns/op 244.74 MB/s 2311 B/op 1 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 25786026 44.71 ns/op 45849.62 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 27173578 44.15 ns/op 46427.72 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 27221428 44.54 ns/op 46030.74 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 27213686 44.07 ns/op 46519.79 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 27233990 44.27 ns/op 46310.26 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAfalse_CacheHit-8 27164018 44.12 ns/op 46460.92 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 19785 60051 ns/op 34.14 MB/s 1386 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 20198 60161 ns/op 34.08 MB/s 1391 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 19585 60345 ns/op 33.97 MB/s 1390 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 19956 61714 ns/op 33.22 MB/s 1391 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 19554 61682 ns/op 33.24 MB/s 1388 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_NoCache-8 19830 60050 ns/op 34.14 MB/s 1393 B/op 9 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 38818 29507 ns/op 69.48 MB/s 3059 B/op 6 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 1000000 58539 ns/op 35.02 MB/s 3835 B/op 10 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 2186653 33757 ns/op 60.73 MB/s 3157 B/op 6 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 1000000 1283 ns/op 1597.72 MB/s 2311 B/op 1 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 1653430 1256 ns/op 1632.67 MB/s 2311 B/op 1 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheMiss-8 2195628 2716 ns/op 754.79 MB/s 2317 B/op 2 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 26531894 44.76 ns/op 45801.05 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 26634384 44.68 ns/op 45878.57 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 27184633 44.97 ns/op 45583.96 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 27011893 44.46 ns/op 46104.62 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 27183812 44.09 ns/op 46498.94 MB/s 0 B/op 0 allocs/op BenchmarkWidthFunction/LongASCIIWithANSI_EAtrue_CacheHit-8 27269318 44.17 ns/op 46406.38 MB/s 0 B/op 0 allocs/op PASS ok github.com/olekukonko/tablewriter/pkg/twwidth 724.296s ? github.com/olekukonko/tablewriter/renderer [no test files] PASS ok github.com/olekukonko/tablewriter/tests 2.959s PASS ok github.com/olekukonko/tablewriter/tw 0.270s tablewriter-1.1.4/tests/streamer_test.go000066400000000000000000000471741515176644300204440ustar00rootroot00000000000000package tests import ( "bytes" "fmt" "strings" "testing" "time" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) // 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.1.4/tests/struct_test.go000066400000000000000000000233331515176644300201350ustar00rootroot00000000000000package tests import ( "bytes" "fmt" "strconv" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) // 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{ strconv.Itoa(emp.ID), emp.Name, strconv.Itoa(emp.Age), emp.Department, strconv.FormatFloat(emp.Salary, 'f', 2, 64), } } // 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.1.4/tests/svg_test.go000066400000000000000000000572451515176644300174210ustar00rootroot00000000000000package 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) { t.Helper() 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{Merging: tw.CellMerging{Mode: 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{Merging: tw.CellMerging{Mode: 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{ Merging: tw.CellMerging{Mode: tw.MergeHorizontal}, Alignment: tw.CellAlignment{Global: tw.AlignCenter}, }, Row: tw.CellConfig{Merging: tw.CellMerging{Mode: 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.1.4/tests/table_bench_test.go000066400000000000000000000051111515176644300210310ustar00rootroot00000000000000package tests import ( "io" "strings" "testing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) 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.1.4/tw/000077500000000000000000000000001515176644300145075ustar00rootroot00000000000000tablewriter-1.1.4/tw/cell.go000066400000000000000000000057421515176644300157650ustar00rootroot00000000000000package tw // CellFormatting holds formatting options for table cells. type CellFormatting struct { AutoWrap int // Wrapping behavior (e.g., WrapTruncate, WrapNormal) AutoFormat State // Enables automatic formatting (e.g., title case for headers) // Deprecated: Kept for backward compatibility. Use CellConfig.CellMerging.Mode instead. // This will be removed in a future version. MergeMode int // Deprecated: Kept for backward compatibility. Use CellConfig.Alignment instead. // This will be removed in a future version. Alignment Align } // CellMerging holds the configuration for how cells should be merged. // This new struct replaces the deprecated MergeMode. type CellMerging struct { // Mode is a bitmask specifying the type of merge (e.g., MergeHorizontal, MergeVertical). Mode int // ByColumnIndex specifies which column indices should be considered for merging. // If the mapper is nil or empty, merging applies to all columns (if Mode is set). // Otherwise, only columns with an index present as a key will be merged. ByColumnIndex Mapper[int, bool] // ByRowIndex is reserved for future features to specify merging on specific rows. ByRowIndex Mapper[int, bool] } // 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 Merging CellMerging // Merging holds all configuration related to cell merging. // 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.1.4/tw/deprecated.go000066400000000000000000000166711515176644300171510ustar00rootroot00000000000000package 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.1.4/tw/fn.go000066400000000000000000000153531515176644300154500ustar00rootroot00000000000000// 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 ( "math" "strconv" "strings" "unicode" "unicode/utf8" "github.com/olekukonko/tablewriter/pkg/twwidth" // For mathematical operations like ceiling // For string-to-number conversions // For string manipulation utilities // For Unicode character classification // 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, 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.1.4/tw/fn_test.go000066400000000000000000000016431515176644300165040ustar00rootroot00000000000000package 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.1.4/tw/mapper.go000066400000000000000000000117421515176644300163270ustar00rootroot00000000000000package 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)) { 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]() 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]() 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]() 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 } func NewBoolMapper[K comparable](keys ...K) Mapper[K, bool] { if len(keys) == 0 { return nil } mapper := NewMapper[K, bool]() for _, key := range keys { mapper.Set(key, true) } return mapper } func NewIntMapper[K comparable](keys ...K) Mapper[K, int] { if len(keys) == 0 { return nil } mapper := NewMapper[K, int]() for _, key := range keys { mapper.Set(key, 0) } return mapper } func NewIdentityMapper[K comparable](keys ...K) Mapper[K, K] { if len(keys) == 0 { return nil } mapper := NewMapper[K, K]() for _, key := range keys { mapper.Set(key, key) } return mapper } tablewriter-1.1.4/tw/preset.go000066400000000000000000000014211515176644300163360ustar00rootroot00000000000000package 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} ) // PaddingDefault represents standard single-space padding on left/right // Equivalent to Padding{Left: " ", Right: " ", Overwrite: true} var PaddingDefault = Padding{Left: " ", Right: " ", Overwrite: true} tablewriter-1.1.4/tw/renderer.go000066400000000000000000000127001515176644300166440ustar00rootroot00000000000000package tw import ( "io" "github.com/olekukonko/ll" ) // 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.1.4/tw/slicer.go000066400000000000000000000065151515176644300163260ustar00rootroot00000000000000package tw import "slices" // 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)) { 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]() 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]() 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 { return slices.ContainsFunc(s, fn) } // 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) { 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.1.4/tw/state.go000066400000000000000000000017301515176644300161570ustar00rootroot00000000000000package 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.1.4/tw/symbols.go000066400000000000000000000603331515176644300165330ustar00rootroot00000000000000package 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.1.4/tw/tw.go000066400000000000000000000051731515176644300154760ustar00rootroot00000000000000package 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 DefaultCacheStringCapacity = 10 * 1024 // 10 KB ) 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.1.4/tw/types.go000066400000000000000000000166761515176644300162220ustar00rootroot00000000000000// Package tw defines types and constants for table formatting and configuration, // including validation logic for various table properties. package tw import ( "bytes" "io" "strconv" "strings" "github.com/olekukonko/errors" ) // 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 } // Counter defines an interface that combines io.Writer with a method to retrieve a total. // This is used by the WithCounter option to allow for counting lines, bytes, etc. type Counter interface { io.Writer // It must be a writer to be used in io.MultiWriter. Total() int } // 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(strconv.Itoa(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 determines trimming of leading and trailing spaces from cell content. TrimLine State // TrimLine determines whether empty visual lines within a cell are collapsed. TrimTab State // TrimTab determines trimming of leading and trailing tabs 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 } // LineCounter is the default implementation of the Counter interface. // It counts the number of newline characters written to it. type LineCounter struct { count int } // Write implements the io.Writer interface, counting newlines in the input. // It uses a pointer receiver to modify the internal count. func (lc *LineCounter) Write(p []byte) (n int, err error) { lc.count += bytes.Count(p, []byte{'\n'}) return len(p), nil } // Total implements the Counter interface, returning the final count. func (lc *LineCounter) Total() int { return lc.count } tablewriter-1.1.4/zoo.go000066400000000000000000001715601515176644300152250ustar00rootroot00000000000000package tablewriter import ( "database/sql" "fmt" "io" "math" "reflect" "strconv" "strings" "github.com/olekukonko/errors" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" ) // 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) { // First, ensure we should even run this logic. // Check both the new CellMerging struct and the deprecated Formatting field. mergeMode := t.config.Row.Merging.Mode if mergeMode == 0 { mergeMode = t.config.Row.Formatting.MergeMode } if !(mergeMode&tw.MergeHierarchical != 0) { return } mergeColumnMapper := t.config.Row.Merging.ByColumnIndex if mergeColumnMapper != nil { ctx.logger.Debugf("Applying hierarchical merges ONLY to specified columns: %v", mergeColumnMapper.Keys()) } else { 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 a column map is specified, skip columns that are not in it. if mergeColumnMapper != nil && !mergeColumnMapper.Has(c) { leftCellContinuedHierarchical = false // Reset hierarchy tracking continue } 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") } // applyHorizontalMerges adjusts column widths for horizontal merges. // Parameters include position, ctx for rendering, and mergeStates for merges. // No return value. func (t *Table) applyHorizontalMerges(position tw.Position, ctx *renderContext, mergeStates map[int]tw.MergeState) { if mergeStates == nil { t.logger.Debugf("applyHorizontalMerges: Skipping %s - no merge states", position) return } t.logger.Debugf("applyHorizontalMerges: 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("applyHorizontalMerges: 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) { // First, ensure we should even run this logic. // Check both the new CellMerging struct and the deprecated Formatting field. mergeMode := t.config.Row.Merging.Mode if mergeMode == 0 { mergeMode = t.config.Row.Formatting.MergeMode } if !(mergeMode&tw.MergeVertical != 0) { return } mergeColumnMapper := t.config.Row.Merging.ByColumnIndex if mergeColumnMapper != nil { ctx.logger.Debugf("Applying vertical merges ONLY to specified columns: %v", mergeColumnMapper.Keys()) } else { 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++ { // If a column map is specified, skip columns that are not in it. if mergeColumnMapper != nil && !mergeColumnMapper.Has(col) { continue } // 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 := max(twwidth.Width(padChar), 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()) // 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(_, 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(_, 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, 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(_, 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 := max(t.config.Widths.Global-numSeparators, 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, 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, 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 := max(t.streamWidths.Get(colIdx), 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 // 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) } // 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) } } // 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) } } // 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) } // 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) // Cache lookup using twcache.LRU // This assumes t.stringerCache is *twcache.LRU[reflect.Type, reflect.Value] if t.stringerCache != nil { if cachedFunc, ok := t.stringerCache.Get(inputType); ok { t.logger.Debugf("convertToStringer: Cache hit for type %v", inputType) // We can proceed to call it immediately because it's already been validated/cached results := cachedFunc.Call([]reflect.Value{reflect.ValueOf(input)}) if len(results) == 1 && results[0].Type() == reflect.TypeOf([]string{}) { return results[0].Interface().([]string), nil } } } stringerFuncVal := reflect.ValueOf(t.stringer) stringerFuncType := stringerFuncVal.Type() // 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 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. callArgs = []reflect.Value{reflect.Zero(paramType)} } else { callArgs = []reflect.Value{reflect.ValueOf(input)} } resultValues := stringerFuncVal.Call(callArgs) // Add to cache if enabled (not nil) and input type is valid if t.stringerCache != nil && inputType != nil { t.stringerCache.Add(inputType, stringerFuncVal) } 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. func (t *Table) convertItemToCells(item interface{}) ([]string, error) { t.logger.Debugf("convertItemToCells: Converting item of type %T", item) // 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) } // Handle untyped nil directly. if item == nil { t.logger.Debugf("convertItemToCells: Item is untyped nil. Returning single empty cell.") return []string{""}, nil } // 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 } // 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(processedRows [][][]string, 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(processedRows), numOriginalCols) for rowIdx, logicalRow := range processedRows { 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 } // 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 { // Always measure the raw line width, because the renderer // will receive the raw line. Do not trim before measuring. lineWidth := twwidth.Width(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 }