├── .travis.yml ├── LICENSE ├── NOTICE ├── README.md ├── cell.go ├── cell_test.go ├── html.go ├── html_test.go ├── row.go ├── row_test.go ├── separator.go ├── straight_separator.go ├── style.go ├── table.go └── table_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.3 4 | - 1.4 5 | 6 | install: 7 | - go get golang.org/x/tools/cmd/vet 8 | 9 | script: 10 | - go fmt ./... 11 | - go vet ./... 12 | - go test -i -race ./... 13 | - go test -v -race ./... 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | This is a legit fork of https://github.com/apcera/termtables. Redistribution based on LICENSE. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tablewriter 2 | 3 | This is a legit fork of [apcera/termtables](https://github.com/apcera/termtables). This version is portable 4 | and does not rely on platform-dependent stuff like locale and term info. 5 | 6 | A [Go](http://golang.org) port of the Ruby library [terminal-tables](https://github.com/visionmedia/terminal-table) for fast and simple ASCII table generation. 7 | 8 | ## Installation 9 | 10 | ```bash 11 | go get github.com/xlab/tablewriter 12 | ``` 13 | 14 | ## Go Style Documentation 15 | 16 | [http://godoc.org/github.com/xlab/tablewriter](http://godoc.org/github.com/xlab/tablewriter) 17 | 18 | ## Basic Usage 19 | 20 | ```go 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "github.com/xlab/tablewriter" 26 | ) 27 | 28 | func main() { 29 | table := tablewriter.CreateTable() 30 | 31 | table.AddHeaders("Name", "Age") 32 | table.AddRow("John", "30") 33 | table.AddRow("Sam", 18) 34 | table.AddRow("Julie", 20.14) 35 | 36 | fmt.Println(table.Render()) 37 | } 38 | ``` 39 | 40 | Result: 41 | 42 | ``` 43 | +-------+-------+ 44 | | Name | Age | 45 | +-------+-------+ 46 | | John | 30 | 47 | | Sam | 18 | 48 | | Julie | 20.14 | 49 | +-------+-------+ 50 | ``` 51 | 52 | ## Advanced Usage 53 | 54 | The package function-call `EnableUTF8()` will cause any tables created after 55 | that point to use Unicode box-drawing characters for the table lines. 56 | 57 | Calling `SetModeHTML(true)` will cause any tables created after that point 58 | to be emitted in HTML, while `SetModeMarkdown(true)` will trigger Markdown. 59 | Neither should result in changes to later API to get the different results; 60 | the primary intended use-case is extracting the same table, but for 61 | documentation. 62 | 63 | The table method `.AddSeparator()` inserts a rule line in the output. This 64 | only applies in normal terminal output mode. 65 | 66 | The table method `.AddTitle()` adds a title to the table; in terminal output, 67 | this is an initial row; in HTML, it's a caption. In Markdown, it's a line of 68 | text before the table, prefixed by `Table: `. 69 | 70 | The table method `.SetAlign()` takes an alignment and a column number 71 | (indexing starts at 1) and changes all _current_ cells in that column to have 72 | the given alignment. It does not change the alignment of cells added to the 73 | table after this call. Alignment is only stored on a per-cell basis. 74 | 75 | ## Known Issues 76 | 77 | Normal output: 78 | 79 | * `.SetAlign()` does not affect headers. 80 | 81 | Markdown output mode: 82 | 83 | * When emitting Markdown, the column markers are not re-flowed if a vertical 84 | bar is an element of a cell, causing an escape to take place; since Markdown 85 | is often converted to HTML, this only affects text viewing. 86 | * A title in Markdown is not escaped against all possible forms of Markdown 87 | markup (to avoid adding a dependency upon a Markdown library, as supported 88 | syntax can vary). 89 | * Markdown requires headers, so a dummy header will be inserted if needed. 90 | * Table alignment is not reflected in Markdown output. 91 | -------------------------------------------------------------------------------- /cell.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Apcera Inc. All rights reserved. 2 | 3 | package tablewriter 4 | 5 | import ( 6 | "fmt" 7 | "math" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "unicode/utf8" 12 | ) 13 | 14 | var ( 15 | // Must match SGR escape sequence, which is "CSI Pm m", where the Control 16 | // Sequence Introducer (CSI) is "ESC ["; where Pm is "A multiple numeric 17 | // parameter composed of any number of single numeric parameters, separated 18 | // by ; character(s). Individual values for the parameters are listed with 19 | // Ps" and where Ps is A single (usually optional) numeric parameter, 20 | // composed of one of [sic] more digits." 21 | // 22 | // In practice, the end sequence is usually given as \e[0m but reading that 23 | // definition, it's clear that the 0 is optional and some testing confirms 24 | // that it is certainly optional with MacOS Terminal 2.3, so we need to 25 | // support the string \e[m as a terminator too. 26 | colorFilter = regexp.MustCompile(`\033\[(?:\d+(?:;\d+)*)?m`) 27 | ) 28 | 29 | // A Cell denotes one cell of a table; it spans one row and a variable number 30 | // of columns. A given Cell can only be used at one place in a table; the act 31 | // of adding the Cell to the table mutates it with position information, so 32 | // do not create one "const" Cell to add it multiple times. 33 | type Cell struct { 34 | column int 35 | formattedValue string 36 | alignment *tableAlignment 37 | colSpan int 38 | } 39 | 40 | // CreateCell returns a Cell where the content is the supplied value, with the 41 | // optional supplied style (which may be given as nil). The style can include 42 | // a non-zero ColSpan to cause the cell to become column-spanning. Changing 43 | // the style afterwards will not adjust the column-spanning state of the cell 44 | // itself. 45 | func CreateCell(v interface{}, style *CellStyle) *Cell { 46 | return createCell(0, v, style) 47 | } 48 | 49 | func createCell(column int, v interface{}, style *CellStyle) *Cell { 50 | cell := &Cell{column: column, formattedValue: renderValue(v), colSpan: 1} 51 | if style != nil { 52 | cell.alignment = &style.Alignment 53 | if style.ColSpan != 0 { 54 | cell.colSpan = style.ColSpan 55 | } 56 | } 57 | return cell 58 | } 59 | 60 | // Width returns the width of the content of the cell, measured in runes; if 61 | // each rune is a single rendering glyph and not "wide", then this is 62 | // sufficient to calculate the width for rendering purposes. This will fail 63 | // on more sophisticated Unicode; in which case, this is the place to plug in 64 | // better logic for "measuring" the display width. Around about then, you 65 | // run into some fundamental limitations of a cell grid display model as is 66 | // used in ttys. 67 | func (c *Cell) Width() int { 68 | return utf8.RuneCountInString(filterColorCodes(c.formattedValue)) 69 | } 70 | 71 | // Filter out terminal bold/color sequences in a string. 72 | // This supports only basic bold/color escape sequences. 73 | func filterColorCodes(s string) string { 74 | return colorFilter.ReplaceAllString(s, "") 75 | } 76 | 77 | // Render returns a string representing the content of the cell, together with 78 | // padding (to the widths specified) and handling any alignment. 79 | func (c *Cell) Render(style *renderStyle) (buffer string) { 80 | // if no alignment is set, import the table's default 81 | if c.alignment == nil { 82 | c.alignment = &style.Alignment 83 | } 84 | 85 | // left padding 86 | buffer += strings.Repeat(" ", style.PaddingLeft) 87 | 88 | // append the main value and handle alignment 89 | buffer += c.alignCell(style) 90 | 91 | // right padding 92 | buffer += strings.Repeat(" ", style.PaddingRight) 93 | 94 | // this handles escaping for, eg, Markdown, where we don't care about the 95 | // alignment quite as much 96 | if style.replaceContent != nil { 97 | buffer = style.replaceContent(buffer) 98 | } 99 | 100 | return buffer 101 | } 102 | 103 | func (c *Cell) alignCell(style *renderStyle) string { 104 | buffer := "" 105 | width := style.CellWidth(c.column) 106 | 107 | if c.colSpan > 1 { 108 | for i := 1; i < c.colSpan; i++ { 109 | w := style.CellWidth(c.column + i) 110 | if w == 0 { 111 | break 112 | } 113 | width += style.PaddingLeft + w + style.PaddingRight + utf8.RuneCountInString(style.BorderY) 114 | } 115 | } 116 | 117 | switch *c.alignment { 118 | 119 | default: 120 | buffer += c.formattedValue 121 | if l := width - c.Width(); l > 0 { 122 | buffer += strings.Repeat(" ", l) 123 | } 124 | 125 | case AlignLeft: 126 | buffer += c.formattedValue 127 | if l := width - c.Width(); l > 0 { 128 | buffer += strings.Repeat(" ", l) 129 | } 130 | 131 | case AlignRight: 132 | if l := width - c.Width(); l > 0 { 133 | buffer += strings.Repeat(" ", l) 134 | } 135 | buffer += c.formattedValue 136 | 137 | case AlignCenter: 138 | left, right := 0, 0 139 | if l := width - c.Width(); l > 0 { 140 | lf := float64(l) 141 | left = int(math.Floor(lf / 2)) 142 | right = int(math.Ceil(lf / 2)) 143 | } 144 | buffer += strings.Repeat(" ", left) 145 | buffer += c.formattedValue 146 | buffer += strings.Repeat(" ", right) 147 | } 148 | 149 | return buffer 150 | } 151 | 152 | // Format the raw value as a string depending on the type 153 | func renderValue(v interface{}) string { 154 | switch vv := v.(type) { 155 | case string: 156 | return vv 157 | case bool: 158 | return strconv.FormatBool(vv) 159 | case int: 160 | return strconv.Itoa(vv) 161 | case int64: 162 | return strconv.FormatInt(vv, 10) 163 | case uint64: 164 | return strconv.FormatUint(vv, 10) 165 | case float64: 166 | return strconv.FormatFloat(vv, 'f', 2, 64) 167 | case fmt.Stringer: 168 | return vv.String() 169 | } 170 | return fmt.Sprintf("%v", v) 171 | } 172 | -------------------------------------------------------------------------------- /cell_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012-2015 Apcera Inc. All rights reserved. 2 | 3 | package tablewriter 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func TestCellRenderString(t *testing.T) { 10 | style := &renderStyle{TableStyle: TableStyle{}, cellWidths: map[int]int{}} 11 | cell := createCell(0, "foobar", nil) 12 | 13 | output := cell.Render(style) 14 | if output != "foobar" { 15 | t.Fatal("Unexpected output:", output) 16 | } 17 | } 18 | 19 | func TestCellRenderBool(t *testing.T) { 20 | style := &renderStyle{TableStyle: TableStyle{}, cellWidths: map[int]int{}} 21 | cell := createCell(0, true, nil) 22 | 23 | output := cell.Render(style) 24 | if output != "true" { 25 | t.Fatal("Unexpected output:", output) 26 | } 27 | } 28 | 29 | func TestCellRenderInteger(t *testing.T) { 30 | style := &renderStyle{TableStyle: TableStyle{}, cellWidths: map[int]int{}} 31 | cell := createCell(0, 12345, nil) 32 | 33 | output := cell.Render(style) 34 | if output != "12345" { 35 | t.Fatal("Unexpected output:", output) 36 | } 37 | } 38 | 39 | func TestCellRenderFloat(t *testing.T) { 40 | style := &renderStyle{TableStyle: TableStyle{}, cellWidths: map[int]int{}} 41 | cell := createCell(0, 12.345, nil) 42 | 43 | output := cell.Render(style) 44 | if output != "12.35" { 45 | t.Fatal("Unexpected output:", output) 46 | } 47 | } 48 | 49 | func TestCellRenderPadding(t *testing.T) { 50 | style := &renderStyle{TableStyle: TableStyle{PaddingLeft: 3, PaddingRight: 4}, cellWidths: map[int]int{}} 51 | 52 | cell := createCell(0, "foobar", nil) 53 | 54 | output := cell.Render(style) 55 | if output != " foobar " { 56 | t.Fatal("Unexpected output:", output) 57 | } 58 | } 59 | 60 | type foo struct { 61 | v string 62 | } 63 | 64 | func (f *foo) String() string { 65 | return f.v 66 | } 67 | 68 | func TestCellRenderStringerStruct(t *testing.T) { 69 | style := &renderStyle{TableStyle: TableStyle{}, cellWidths: map[int]int{}} 70 | cell := createCell(0, &foo{v: "bar"}, nil) 71 | 72 | output := cell.Render(style) 73 | if output != "bar" { 74 | t.Fatal("Unexpected output:", output) 75 | } 76 | } 77 | 78 | type fooString string 79 | 80 | func TestCellRenderGeneric(t *testing.T) { 81 | style := &renderStyle{TableStyle: TableStyle{}, cellWidths: map[int]int{}} 82 | cell := createCell(0, fooString("baz"), nil) 83 | 84 | output := cell.Render(style) 85 | if output != "baz" { 86 | t.Fatal("Unexpected output:", output) 87 | } 88 | } 89 | 90 | func TestFilterColorCodes(t *testing.T) { 91 | tests := []struct { 92 | in string 93 | out string 94 | }{ 95 | {"abc", "abc"}, 96 | {"", ""}, 97 | {"\033[31m\033[0m", ""}, 98 | {"a\033[31mb\033[0mc", "abc"}, 99 | {"\033[31mabc\033[0m", "abc"}, 100 | {"\033[31mfoo\033[0mbar", "foobar"}, 101 | {"\033[31mfoo\033[mbar", "foobar"}, 102 | {"\033[31mfoo\033[0;0mbar", "foobar"}, 103 | {"\033[31;4mfoo\033[0mbar", "foobar"}, 104 | {"\033[31;4;43mfoo\033[0mbar", "foobar"}, 105 | } 106 | for _, test := range tests { 107 | got := filterColorCodes(test.in) 108 | if got != test.out { 109 | t.Errorf("Invalid color-code filter result; expected %q but got %q from input %q", 110 | test.out, got, test.in) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /html.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Apcera Inc. All rights reserved. 2 | 3 | package tablewriter 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "html" 9 | "strings" 10 | ) 11 | 12 | type titleStyle int 13 | 14 | const ( 15 | TitleAsCaption titleStyle = iota 16 | TitleAsThSpan 17 | ) 18 | 19 | // htmlStyleRules defines attributes which we can use, and might be set on a 20 | // table by accessors, to influence the type of HTML which is output. 21 | type htmlStyleRules struct { 22 | title titleStyle 23 | } 24 | 25 | // HTML returns an HTML representations of the contents of one row of a table. 26 | func (r *Row) HTML(tag string, style *renderStyle) string { 27 | attrs := make([]string, len(r.cells)) 28 | elems := make([]string, len(r.cells)) 29 | for i := range r.cells { 30 | if r.cells[i].alignment != nil { 31 | switch *r.cells[i].alignment { 32 | case AlignLeft: 33 | attrs[i] = " align='left'" 34 | case AlignCenter: 35 | attrs[i] = " align='center'" 36 | case AlignRight: 37 | attrs[i] = " align='right'" 38 | } 39 | } 40 | elems[i] = html.EscapeString(strings.TrimSpace(r.cells[i].Render(style))) 41 | } 42 | // WAG as to max capacity, plus a bit 43 | buf := bytes.NewBuffer(make([]byte, 0, 8192)) 44 | buf.WriteString("") 45 | for i := range elems { 46 | fmt.Fprintf(buf, "<%s%s>%s", tag, attrs[i], elems[i], tag) 47 | } 48 | buf.WriteString("\n") 49 | return buf.String() 50 | } 51 | 52 | func generateHtmlTitleRow(title interface{}, t *Table, style *renderStyle) string { 53 | elContent := html.EscapeString( 54 | strings.TrimSpace(CreateCell(t.title, &CellStyle{}).Render(style)), 55 | ) 56 | 57 | switch style.htmlRules.title { 58 | case TitleAsCaption: 59 | return "" + elContent + "\n" 60 | case TitleAsThSpan: 61 | return fmt.Sprintf("%s\n", 62 | style.columns, elContent) 63 | default: 64 | return "" 65 | } 66 | } 67 | 68 | // RenderHTML returns a string representation of a the table, suitable for 69 | // inclusion as HTML elsewhere. Primary use-case controlling layout style 70 | // is for inclusion into Markdown documents, documenting normal table use. 71 | // Thus we leave the padding in place to have columns align when viewed as 72 | // plain text and rely upon HTML ignoring extra whitespace. 73 | func (t *Table) RenderHTML() (buffer string) { 74 | // elements is already populated with row data 75 | 76 | // generate the runtime style 77 | style := createRenderStyle(t) 78 | style.PaddingLeft = 0 79 | style.PaddingRight = 0 80 | 81 | // TODO: control CSS styles to suppress border based upon t.Style.SkipBorder 82 | rowsText := make([]string, 0, len(t.elements)+6) 83 | 84 | if t.title != nil || t.headers != nil { 85 | rowsText = append(rowsText, "\n") 86 | if t.title != nil { 87 | rowsText = append(rowsText, generateHtmlTitleRow(t.title, t, style)) 88 | } 89 | if t.headers != nil { 90 | rowsText = append(rowsText, CreateRow(t.headers).HTML("th", style)) 91 | } 92 | rowsText = append(rowsText, "\n") 93 | } 94 | 95 | rowsText = append(rowsText, "\n") 96 | // loop over the elements and render them 97 | for i := range t.elements { 98 | if row, ok := t.elements[i].(*Row); ok { 99 | rowsText = append(rowsText, row.HTML("td", style)) 100 | } else { 101 | rowsText = append(rowsText, fmt.Sprintf("\n", i)) 102 | } 103 | } 104 | rowsText = append(rowsText, "\n") 105 | 106 | return "\n" + strings.Join(rowsText, "") + "
\n" 107 | } 108 | -------------------------------------------------------------------------------- /html_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Apcera Inc. All rights reserved. 2 | 3 | package tablewriter 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func TestCreateTableHTML(t *testing.T) { 10 | expected := "\n" + 11 | "\n" + 12 | "\n" + 13 | "\n" + 14 | "\n" + 15 | "\n" + 16 | "\n" + 17 | "\n" + 18 | "\n" + 19 | "\n" + 20 | "
NameValue
heyyou
ken1234
derek3.14
derek too3.15
\n" 21 | 22 | table := CreateTable() 23 | table.SetModeHTML() 24 | 25 | table.AddHeaders("Name", "Value") 26 | table.AddRow("hey", "you") 27 | table.AddRow("ken", 1234) 28 | table.AddRow("derek", 3.14) 29 | table.AddRow("derek too", 3.1456788) 30 | 31 | output := table.Render() 32 | if output != expected { 33 | t.Fatal(DisplayFailedOutput(output, expected)) 34 | } 35 | } 36 | 37 | func TestTableWithHeaderHTML(t *testing.T) { 38 | expected := "\n" + 39 | "\n" + 40 | "\n" + 41 | "\n" + 42 | "\n" + 43 | "\n" + 44 | "\n" + 45 | "\n" + 46 | "\n" + 47 | "\n" + 48 | "\n" + 49 | "
Example
NameValue
heyyou
ken1234
derek3.14
derek too3.15
\n" 50 | 51 | table := CreateTable() 52 | table.SetModeHTML() 53 | 54 | table.AddTitle("Example") 55 | table.AddHeaders("Name", "Value") 56 | table.AddRow("hey", "you") 57 | table.AddRow("ken", 1234) 58 | table.AddRow("derek", 3.14) 59 | table.AddRow("derek too", 3.1456788) 60 | 61 | output := table.Render() 62 | if output != expected { 63 | t.Fatal(DisplayFailedOutput(output, expected)) 64 | } 65 | } 66 | 67 | func TestTableTitleWidthAdjustsHTML(t *testing.T) { 68 | expected := "\n" + 69 | "\n" + 70 | "\n" + 71 | "\n" + 72 | "\n" + 73 | "\n" + 74 | "\n" + 75 | "\n" + 76 | "\n" + 77 | "\n" + 78 | "\n" + 79 | "
Example My Foo Bar'd Test
NameValue
heyyou
ken1234
derek3.14
derek too3.15
\n" 80 | 81 | table := CreateTable() 82 | table.SetModeHTML() 83 | 84 | table.AddTitle("Example My Foo Bar'd Test") 85 | table.AddHeaders("Name", "Value") 86 | table.AddRow("hey", "you") 87 | table.AddRow("ken", 1234) 88 | table.AddRow("derek", 3.14) 89 | table.AddRow("derek too", 3.1456788) 90 | 91 | output := table.Render() 92 | if output != expected { 93 | t.Fatal(DisplayFailedOutput(output, expected)) 94 | } 95 | } 96 | 97 | func TestTableWithNoHeadersHTML(t *testing.T) { 98 | expected := "\n" + 99 | "\n" + 100 | "\n" + 101 | "\n" + 102 | "\n" + 103 | "\n" + 104 | "\n" + 105 | "
heyyou
ken1234
derek3.14
derek too3.15
\n" 106 | 107 | table := CreateTable() 108 | table.SetModeHTML() 109 | 110 | table.AddRow("hey", "you") 111 | table.AddRow("ken", 1234) 112 | table.AddRow("derek", 3.14) 113 | table.AddRow("derek too", 3.1456788) 114 | 115 | output := table.Render() 116 | if output != expected { 117 | t.Fatal(DisplayFailedOutput(output, expected)) 118 | } 119 | } 120 | 121 | func TestTableUnicodeWidthsHTML(t *testing.T) { 122 | expected := "\n" + 123 | "\n" + 124 | "\n" + 125 | "\n" + 126 | "\n" + 127 | "\n" + 128 | "\n" + 129 | "\n" + 130 | "\n" + 131 | "\n" + 132 | "
NameCost
Currency¤10
US Dollar$30
Euro€27
Thai฿70
\n" 133 | 134 | table := CreateTable() 135 | table.SetModeHTML() 136 | table.AddHeaders("Name", "Cost") 137 | table.AddRow("Currency", "¤10") 138 | table.AddRow("US Dollar", "$30") 139 | table.AddRow("Euro", "€27") 140 | table.AddRow("Thai", "฿70") 141 | 142 | output := table.Render() 143 | if output != expected { 144 | t.Fatal(DisplayFailedOutput(output, expected)) 145 | } 146 | } 147 | 148 | func TestTableWithAlignment(t *testing.T) { 149 | expected := "\n" + 150 | "\n" + 151 | "\n" + 152 | "\n" + 153 | "\n" + 154 | "\n" + 155 | "\n" + 156 | "\n" + 157 | "
FooBar
humptydumpty
r<- on right
\n" 158 | 159 | table := CreateTable() 160 | table.SetModeHTML() 161 | table.AddHeaders("Foo", "Bar") 162 | table.AddRow("humpty", "dumpty") 163 | table.AddRow(CreateCell("r", &CellStyle{Alignment: AlignRight}), "<- on right") 164 | 165 | output := table.Render() 166 | if output != expected { 167 | t.Fatal(DisplayFailedOutput(output, expected)) 168 | } 169 | } 170 | 171 | func TestTableAfterSetAlign(t *testing.T) { 172 | expected := "\n" + 173 | "\n" + 174 | "\n" + 175 | "\n" + 176 | "\n" + 177 | "\n" + 178 | "\n" + 179 | "\n" + 180 | "\n" + 181 | "
AlphabeticalNum
alfa1
bravo2
charlie3
\n" 182 | 183 | table := CreateTable() 184 | table.SetModeHTML() 185 | table.AddHeaders("Alphabetical", "Num") 186 | table.AddRow("alfa", 1) 187 | table.AddRow("bravo", 2) 188 | table.AddRow("charlie", 3) 189 | table.SetAlign(AlignRight, 1) 190 | 191 | output := table.Render() 192 | if output != expected { 193 | t.Fatal(DisplayFailedOutput(output, expected)) 194 | } 195 | } 196 | 197 | func TestTableWithAltTitleStyle(t *testing.T) { 198 | expected := "" + 199 | "\n" + 200 | "\n" + 201 | "\n" + 202 | "\n" + 203 | "\n" + 204 | "\n" + 205 | "\n" + 206 | "\n" + 207 | "\n" + 208 | "
Metasyntactic
FooBarBaz
abc
αβγ
\n" 209 | 210 | table := CreateTable() 211 | table.SetModeHTML() 212 | table.SetHTMLStyleTitle(TitleAsThSpan) 213 | table.AddTitle("Metasyntactic") 214 | table.AddHeaders("Foo", "Bar", "Baz") 215 | table.AddRow("a", "b", "c") 216 | table.AddRow("α", "β", "γ") 217 | 218 | output := table.Render() 219 | if output != expected { 220 | t.Fatal(DisplayFailedOutput(output, expected)) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /row.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Apcera Inc. All rights reserved. 2 | 3 | package tablewriter 4 | 5 | import "strings" 6 | 7 | // A Row represents one row of a Table, consisting of some number of Cell 8 | // items. 9 | type Row struct { 10 | cells []*Cell 11 | } 12 | 13 | // CreateRow returns a Row where the cells are created as needed to hold each 14 | // item given; each item can be a Cell or content to go into a Cell created 15 | // to hold it. 16 | func CreateRow(items []interface{}) *Row { 17 | row := &Row{cells: []*Cell{}} 18 | for _, item := range items { 19 | row.AddCell(item) 20 | } 21 | return row 22 | } 23 | 24 | // AddCell adds one item to a row as a new cell, where the item is either a 25 | // Cell or content to be put into a cell. 26 | func (r *Row) AddCell(item interface{}) { 27 | if c, ok := item.(*Cell); ok { 28 | c.column = len(r.cells) 29 | r.cells = append(r.cells, c) 30 | } else { 31 | r.cells = append(r.cells, createCell(len(r.cells), item, nil)) 32 | } 33 | } 34 | 35 | // Render returns a string representing the content of one row of a table, where 36 | // the Row contains Cells (not Separators) and the representation includes any 37 | // vertical borders needed. 38 | func (r *Row) Render(style *renderStyle) string { 39 | // pre-render and shove into an array... helps with cleanly adding borders 40 | renderedCells := []string{} 41 | for _, c := range r.cells { 42 | renderedCells = append(renderedCells, c.Render(style)) 43 | } 44 | 45 | // format final output 46 | return style.BorderY + strings.Join(renderedCells, style.BorderY) + style.BorderY 47 | } 48 | -------------------------------------------------------------------------------- /row_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012-2015 Apcera Inc. All rights reserved. 2 | 3 | package tablewriter 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func TestBasicRowRender(t *testing.T) { 10 | row := CreateRow([]interface{}{"foo", "bar"}) 11 | style := &renderStyle{TableStyle: TableStyle{BorderX: "-", BorderY: "|", BorderI: "+", 12 | PaddingLeft: 1, PaddingRight: 1}, cellWidths: map[int]int{0: 3, 1: 3}} 13 | 14 | output := row.Render(style) 15 | if output != "| foo | bar |" { 16 | t.Fatal("Unexpected output:", output) 17 | } 18 | } 19 | 20 | func TestRowRenderWidthBasedPadding(t *testing.T) { 21 | row := CreateRow([]interface{}{"foo", "bar"}) 22 | style := &renderStyle{TableStyle: TableStyle{BorderX: "-", BorderY: "|", BorderI: "+", 23 | PaddingLeft: 1, PaddingRight: 1}, cellWidths: map[int]int{0: 3, 1: 5}} 24 | 25 | output := row.Render(style) 26 | if output != "| foo | bar |" { 27 | t.Fatal("Unexpected output:", output) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /separator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Apcera Inc. All rights reserved. 2 | 3 | package tablewriter 4 | 5 | import "strings" 6 | 7 | type lineType int 8 | 9 | // These lines are for horizontal rules; these indicate desired styling, 10 | // but simplistic (pure ASCII) markup characters may end up leaving the 11 | // variant lines indistinguishable from LINE_INNER. 12 | const ( 13 | // LINE_INNER *must* be the default; where there are vertical lines drawn 14 | // across an inner line, the character at that position should indicate 15 | // that the vertical line goes both up and down from this horizontal line. 16 | LINE_INNER lineType = iota 17 | 18 | // LINE_TOP has only descenders 19 | LINE_TOP 20 | 21 | // LINE_SUBTOP has only descenders in the middle, but goes both up and 22 | // down at the far left & right edges. 23 | LINE_SUBTOP 24 | 25 | // LINE_BOTTOM has only ascenders. 26 | LINE_BOTTOM 27 | ) 28 | 29 | // A Separator is a horizontal rule line, with associated information which 30 | // indicates where in a table it is, sufficient for simple cases to let 31 | // clean tables be drawn. If a row-spanning cell is created, then this will 32 | // be insufficient: we can get away with hand-waving of "well, it's showing 33 | // where the border would be" but a more capable handling will require 34 | // structure reworking. Patches welcome. 35 | type Separator struct { 36 | where lineType 37 | } 38 | 39 | // Render returns the string representation of a horizontal rule line in the 40 | // table. 41 | func (s *Separator) Render(style *renderStyle) string { 42 | // loop over getting dashes 43 | parts := []string{} 44 | for i := 0; i < style.columns; i++ { 45 | w := style.PaddingLeft + style.CellWidth(i) + style.PaddingRight 46 | parts = append(parts, strings.Repeat(style.BorderX, w)) 47 | } 48 | 49 | switch s.where { 50 | case LINE_TOP: 51 | return style.BorderTopLeft + strings.Join(parts, style.BorderTop) + style.BorderTopRight 52 | case LINE_SUBTOP: 53 | return style.BorderLeft + strings.Join(parts, style.BorderTop) + style.BorderRight 54 | case LINE_BOTTOM: 55 | return style.BorderBottomLeft + strings.Join(parts, style.BorderBottom) + style.BorderBottomRight 56 | case LINE_INNER: 57 | return style.BorderLeft + strings.Join(parts, style.BorderI) + style.BorderRight 58 | } 59 | panic("not reached") 60 | } 61 | -------------------------------------------------------------------------------- /straight_separator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Apcera Inc. All rights reserved. 2 | 3 | package tablewriter 4 | 5 | import ( 6 | "strings" 7 | "unicode/utf8" 8 | ) 9 | 10 | // A StraightSeparator is a horizontal line with associated information about 11 | // what sort of position it takes in the table, so as to control which shapes 12 | // will be used where vertical lines are expected to touch this horizontal 13 | // line. 14 | type StraightSeparator struct { 15 | where lineType 16 | } 17 | 18 | // Render returns a string representing this separator, with all border 19 | // crossings appropriately chosen. 20 | func (s *StraightSeparator) Render(style *renderStyle) string { 21 | // loop over getting dashes 22 | width := 0 23 | for i := 0; i < style.columns; i++ { 24 | width += style.PaddingLeft + style.CellWidth(i) + style.PaddingRight + utf8.RuneCountInString(style.BorderI) 25 | } 26 | 27 | switch s.where { 28 | case LINE_TOP: 29 | return style.BorderTopLeft + strings.Repeat(style.BorderX, width-1) + style.BorderTopRight 30 | case LINE_INNER, LINE_SUBTOP: 31 | return style.BorderLeft + strings.Repeat(style.BorderX, width-1) + style.BorderRight 32 | case LINE_BOTTOM: 33 | return style.BorderBottomLeft + strings.Repeat(style.BorderX, width-1) + style.BorderBottomRight 34 | } 35 | panic("not reached") 36 | } 37 | -------------------------------------------------------------------------------- /style.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012-2013 Apcera Inc. All rights reserved. 2 | 3 | package tablewriter 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | "unicode/utf8" 9 | ) 10 | 11 | type tableAlignment int 12 | 13 | // These constants control the alignment which should be used when rendering 14 | // the content of a cell. 15 | const ( 16 | AlignLeft = tableAlignment(1) 17 | AlignCenter = tableAlignment(2) 18 | AlignRight = tableAlignment(3) 19 | ) 20 | 21 | // TableStyle controls styling information for a Table as a whole. 22 | // 23 | // For the Border rules, only X, Y and I are needed, and all have defaults. 24 | // The others will all default to the same as BorderI. 25 | type TableStyle struct { 26 | SkipBorder bool 27 | BorderX string 28 | BorderY string 29 | BorderI string 30 | BorderTop string 31 | BorderBottom string 32 | BorderRight string 33 | BorderLeft string 34 | BorderTopLeft string 35 | BorderTopRight string 36 | BorderBottomLeft string 37 | BorderBottomRight string 38 | PaddingLeft int 39 | PaddingRight int 40 | Width int 41 | Alignment tableAlignment 42 | htmlRules htmlStyleRules 43 | } 44 | 45 | // A CellStyle controls all style applicable to one Cell. 46 | type CellStyle struct { 47 | // Alignment indicates the alignment to be used in rendering the content 48 | Alignment tableAlignment 49 | 50 | // ColSpan indicates how many columns this Cell is expected to consume. 51 | ColSpan int 52 | } 53 | 54 | // DefaultStyle is a TableStyle which can be used to get some simple 55 | // default styling for a table, using ASCII characters for drawing borders. 56 | var DefaultStyle = &TableStyle{ 57 | SkipBorder: false, 58 | BorderX: "-", BorderY: "|", BorderI: "+", 59 | PaddingLeft: 1, PaddingRight: 1, 60 | Width: 80, 61 | Alignment: AlignLeft, 62 | 63 | // FIXME: the use of a Width here may interact poorly with a changing 64 | // MaxColumns value; we don't set MaxColumns here because the evaluation 65 | // order of a var and an init value adds undesired subtlety. 66 | } 67 | 68 | type renderStyle struct { 69 | cellWidths map[int]int 70 | columns int 71 | 72 | // used for markdown rendering 73 | replaceContent func(string) string 74 | 75 | TableStyle 76 | } 77 | 78 | // setUtfBoxStyle changes the border characters to be suitable for use when 79 | // the output stream can render UTF-8 characters. 80 | func (s *TableStyle) setUtfBoxStyle() { 81 | s.BorderX = "─" 82 | s.BorderY = "│" 83 | s.BorderI = "┼" 84 | s.BorderTop = "┬" 85 | s.BorderBottom = "┴" 86 | s.BorderLeft = "├" 87 | s.BorderRight = "┤" 88 | s.BorderTopLeft = "╭" 89 | s.BorderTopRight = "╮" 90 | s.BorderBottomLeft = "╰" 91 | s.BorderBottomRight = "╯" 92 | } 93 | 94 | // setAsciiBoxStyle changes the border characters back to their defaults 95 | func (s *TableStyle) setAsciiBoxStyle() { 96 | s.BorderX = "-" 97 | s.BorderY = "|" 98 | s.BorderI = "+" 99 | s.BorderTop, s.BorderBottom, s.BorderLeft, s.BorderRight = "", "", "", "" 100 | s.BorderTopLeft, s.BorderTopRight, s.BorderBottomLeft, s.BorderBottomRight = "", "", "", "" 101 | s.fillStyleRules() 102 | } 103 | 104 | // fillStyleRules populates members of the TableStyle box-drawing specification 105 | // with BorderI as the default. 106 | func (s *TableStyle) fillStyleRules() { 107 | if s.BorderTop == "" { 108 | s.BorderTop = s.BorderI 109 | } 110 | if s.BorderBottom == "" { 111 | s.BorderBottom = s.BorderI 112 | } 113 | if s.BorderLeft == "" { 114 | s.BorderLeft = s.BorderI 115 | } 116 | if s.BorderRight == "" { 117 | s.BorderRight = s.BorderI 118 | } 119 | if s.BorderTopLeft == "" { 120 | s.BorderTopLeft = s.BorderI 121 | } 122 | if s.BorderTopRight == "" { 123 | s.BorderTopRight = s.BorderI 124 | } 125 | if s.BorderBottomLeft == "" { 126 | s.BorderBottomLeft = s.BorderI 127 | } 128 | if s.BorderBottomRight == "" { 129 | s.BorderBottomRight = s.BorderI 130 | } 131 | } 132 | 133 | func createRenderStyle(table *Table) *renderStyle { 134 | style := &renderStyle{TableStyle: *table.Style, cellWidths: map[int]int{}} 135 | style.TableStyle.fillStyleRules() 136 | 137 | if table.outputMode == outputMarkdown { 138 | style.buildReplaceContent(table.Style.BorderY) 139 | } 140 | 141 | // FIXME: handle actually defined width condition 142 | 143 | // loop over the rows and cells to calculate widths 144 | for _, element := range table.elements { 145 | // skip separators 146 | if _, ok := element.(*Separator); ok { 147 | continue 148 | } 149 | 150 | // iterate over cells 151 | if row, ok := element.(*Row); ok { 152 | for i, cell := range row.cells { 153 | // FIXME: need to support sizing with colspan handling 154 | if cell.colSpan > 1 { 155 | continue 156 | } 157 | if style.cellWidths[i] < cell.Width() { 158 | style.cellWidths[i] = cell.Width() 159 | } 160 | } 161 | } 162 | } 163 | style.columns = len(style.cellWidths) 164 | 165 | // calculate actual width 166 | width := utf8.RuneCountInString(style.BorderLeft) // start at '1' for left border 167 | internalBorderWidth := utf8.RuneCountInString(style.BorderI) 168 | 169 | lastIndex := 0 170 | for i, v := range style.cellWidths { 171 | width += v + style.PaddingLeft + style.PaddingRight + internalBorderWidth 172 | if i > lastIndex { 173 | lastIndex = i 174 | } 175 | } 176 | if internalBorderWidth != utf8.RuneCountInString(style.BorderRight) { 177 | width += utf8.RuneCountInString(style.BorderRight) - internalBorderWidth 178 | } 179 | 180 | if table.titleCell != nil { 181 | titleMinWidth := 0 + 182 | table.titleCell.Width() + 183 | utf8.RuneCountInString(style.BorderLeft) + 184 | utf8.RuneCountInString(style.BorderRight) + 185 | style.PaddingLeft + 186 | style.PaddingRight 187 | 188 | if width < titleMinWidth { 189 | // minWidth must be set to include padding of the title, as required 190 | style.cellWidths[lastIndex] += (titleMinWidth - width) 191 | width = titleMinWidth 192 | } 193 | } 194 | 195 | // right border is covered in loop 196 | style.Width = width 197 | 198 | return style 199 | } 200 | 201 | // CellWidth returns the width of the cell at the supplied index, where the 202 | // width is the number of tty character-cells required to draw the glyphs. 203 | func (s *renderStyle) CellWidth(i int) int { 204 | return s.cellWidths[i] 205 | } 206 | 207 | // buildReplaceContent creates a function closure, with minimal bound lexical 208 | // state, which replaces content 209 | func (s *renderStyle) buildReplaceContent(bad string) { 210 | replacement := fmt.Sprintf("&#x%02x;", bad) 211 | s.replaceContent = func(old string) string { 212 | return strings.Replace(old, bad, replacement, -1) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /table.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012-2013 Apcera Inc. All rights reserved. 2 | 3 | package tablewriter 4 | 5 | import ( 6 | "strings" 7 | ) 8 | 9 | // MaxColumns represents the maximum number of columns that are available for 10 | // display without wrapping around the right-hand side of the terminal window. 11 | // At program initialization, the value will be automatically set according 12 | // to available sources of information, including the $COLUMNS environment 13 | // variable and, on Unix, tty information. 14 | var MaxColumns = 80 15 | 16 | // An Element is a drawn representation of the contents of a table cell. 17 | type Element interface { 18 | Render(*renderStyle) string 19 | } 20 | 21 | type outputMode int 22 | 23 | const ( 24 | outputTerminal outputMode = iota 25 | outputMarkdown 26 | outputHTML 27 | ) 28 | 29 | // open question: should UTF-8 become an output mode? It does require more 30 | // tracking when resetting, if the locale-enabling had been used 31 | 32 | var outputsEnabled struct { 33 | UTF8 bool 34 | HTML bool 35 | Markdown bool 36 | titleStyle titleStyle 37 | } 38 | 39 | var defaultOutputMode outputMode = outputTerminal 40 | 41 | // Table represents a terminal table. The Style can be directly accessed 42 | // and manipulated; all other access is via methods. 43 | type Table struct { 44 | Style *TableStyle 45 | 46 | elements []Element 47 | headers []interface{} 48 | title interface{} 49 | titleCell *Cell 50 | outputMode outputMode 51 | } 52 | 53 | // EnableUTF8 will unconditionally enable using UTF-8 box-drawing characters 54 | // for any tables created after this call, as the default style. 55 | func EnableUTF8() { 56 | outputsEnabled.UTF8 = true 57 | } 58 | 59 | // SetModeHTML will control whether or not new tables generated will be in HTML 60 | // mode by default; HTML-or-not takes precedence over options which control how 61 | // a terminal output will be rendered, such as whether or not to use UTF8. 62 | // This affects any tables created after this call. 63 | func SetModeHTML(onoff bool) { 64 | outputsEnabled.HTML = onoff 65 | chooseDefaultOutput() 66 | } 67 | 68 | // SetModeMarkdown will control whether or not new tables generated will be 69 | // in Markdown mode by default. HTML-mode takes precedence. 70 | func SetModeMarkdown(onoff bool) { 71 | outputsEnabled.Markdown = onoff 72 | chooseDefaultOutput() 73 | } 74 | 75 | // SetHTMLStyleTitle lets an HTML title output mode be chosen. 76 | func SetHTMLStyleTitle(want titleStyle) { 77 | outputsEnabled.titleStyle = want 78 | } 79 | 80 | // chooseDefaultOutput sets defaultOutputMode based on priority 81 | // choosing amongst the options which are enabled. Pros: simpler 82 | // encapsulation; cons: setting markdown doesn't disable HTML if 83 | // HTML was previously enabled and was later disabled. 84 | // This seems fairly reasonable. 85 | func chooseDefaultOutput() { 86 | if outputsEnabled.HTML { 87 | defaultOutputMode = outputHTML 88 | } else if outputsEnabled.Markdown { 89 | defaultOutputMode = outputMarkdown 90 | } else { 91 | defaultOutputMode = outputTerminal 92 | } 93 | } 94 | 95 | // CreateTable creates an empty Table using defaults for style. 96 | func CreateTable() *Table { 97 | t := &Table{elements: []Element{}, Style: DefaultStyle} 98 | if outputsEnabled.UTF8 { 99 | t.Style.setUtfBoxStyle() 100 | } 101 | if outputsEnabled.titleStyle != titleStyle(0) { 102 | t.Style.htmlRules.title = outputsEnabled.titleStyle 103 | } 104 | t.outputMode = defaultOutputMode 105 | return t 106 | } 107 | 108 | // AddSeparator adds a line to the table content, where the line 109 | // consists of separator characters. 110 | func (t *Table) AddSeparator() { 111 | t.elements = append(t.elements, &Separator{}) 112 | } 113 | 114 | // AddRow adds the supplied items as cells in one row of the table. 115 | func (t *Table) AddRow(items ...interface{}) *Row { 116 | row := CreateRow(items) 117 | t.elements = append(t.elements, row) 118 | return row 119 | } 120 | 121 | // AddTitle supplies a table title, which if present will be rendered as 122 | // one cell across the width of the table, as the first row. 123 | func (t *Table) AddTitle(title interface{}) { 124 | t.title = title 125 | } 126 | 127 | // AddHeaders supplies column headers for the table. 128 | func (t *Table) AddHeaders(headers ...interface{}) { 129 | t.headers = headers[:] 130 | } 131 | 132 | // SetAlign changes the alignment for elements in a column of the table; 133 | // alignments are stored with each cell, so cells added after a call to 134 | // SetAlign will not pick up the change. Columns are numbered from 1. 135 | func (t *Table) SetAlign(align tableAlignment, column int) { 136 | if column < 0 { 137 | return 138 | } 139 | for i := range t.elements { 140 | row, ok := t.elements[i].(*Row) 141 | if !ok { 142 | continue 143 | } 144 | if column >= len(row.cells) { 145 | continue 146 | } 147 | row.cells[column-1].alignment = &align 148 | } 149 | } 150 | 151 | // UTF8Box sets the table style to use UTF-8 box-drawing characters, 152 | // overriding all relevant style elements at the time of the call. 153 | func (t *Table) UTF8Box() { 154 | t.Style.setUtfBoxStyle() 155 | } 156 | 157 | // SetModeHTML switches this table to be in HTML when rendered; the 158 | // default depends upon whether the package function SetModeHTML() has been 159 | // called, and with what value. This method forces the feature on for this 160 | // table. Turning off involves choosing a different mode, per-table. 161 | func (t *Table) SetModeHTML() { 162 | t.outputMode = outputHTML 163 | } 164 | 165 | // SetModeMarkdown switches this table to be in Markdown mode 166 | func (t *Table) SetModeMarkdown() { 167 | t.outputMode = outputMarkdown 168 | } 169 | 170 | // SetModeTerminal switches this table to be in terminal mode 171 | func (t *Table) SetModeTerminal() { 172 | t.outputMode = outputTerminal 173 | } 174 | 175 | // SetHTMLStyleTitle lets an HTML output mode be chosen; we should rework this 176 | // into a more generic and extensible API as we clean up tablewriter 177 | func (t *Table) SetHTMLStyleTitle(want titleStyle) { 178 | t.Style.htmlRules.title = want 179 | } 180 | 181 | // Render returns a string representation of a fully rendered table, drawn 182 | // out for display, with embedded newlines. If this table is in HTML mode, 183 | // then this is equivalent to RenderHTML(). 184 | func (t *Table) Render() (buffer string) { 185 | // elements is already populated with row data 186 | switch t.outputMode { 187 | case outputTerminal: 188 | return t.renderTerminal() 189 | case outputMarkdown: 190 | return t.renderMarkdown() 191 | case outputHTML: 192 | return t.RenderHTML() 193 | default: 194 | panic("unknown output mode set") 195 | } 196 | } 197 | 198 | // renderTerminal returns a string representation of a fully rendered table, 199 | // drawn out for display, with embedded newlines. 200 | func (t *Table) renderTerminal() (buffer string) { 201 | // initial top line 202 | if !t.Style.SkipBorder { 203 | if t.title != nil && t.headers == nil { 204 | t.elements = append([]Element{&Separator{where: LINE_SUBTOP}}, t.elements...) 205 | } else if t.title == nil && t.headers == nil { 206 | t.elements = append([]Element{&Separator{where: LINE_TOP}}, t.elements...) 207 | } else { 208 | t.elements = append([]Element{&Separator{where: LINE_INNER}}, t.elements...) 209 | } 210 | } 211 | 212 | // if we have headers, include them 213 | if t.headers != nil { 214 | ne := make([]Element, 2) 215 | ne[1] = CreateRow(t.headers) 216 | if t.title != nil { 217 | ne[0] = &Separator{where: LINE_SUBTOP} 218 | } else { 219 | ne[0] = &Separator{where: LINE_TOP} 220 | } 221 | t.elements = append(ne, t.elements...) 222 | } 223 | 224 | // if we have a title, write them 225 | if t.title != nil { 226 | // match changes to this into renderMarkdown too 227 | t.titleCell = CreateCell(t.title, &CellStyle{Alignment: AlignCenter, ColSpan: 999}) 228 | ne := []Element{ 229 | &StraightSeparator{where: LINE_TOP}, 230 | CreateRow([]interface{}{t.titleCell}), 231 | } 232 | t.elements = append(ne, t.elements...) 233 | } 234 | 235 | // generate the runtime style 236 | style := createRenderStyle(t) 237 | 238 | // loop over the elements and render them 239 | for _, e := range t.elements { 240 | buffer += e.Render(style) + "\n" 241 | } 242 | 243 | // add bottom line 244 | if !style.SkipBorder { 245 | buffer += (&Separator{where: LINE_BOTTOM}).Render(style) + "\n" 246 | } 247 | 248 | return buffer 249 | } 250 | 251 | // renderMarkdown returns a string representation of a table in Markdown 252 | // markup format using GitHub Flavored Markdown's notation (since tables 253 | // are not in the core Markdown spec). 254 | func (t *Table) renderMarkdown() (buffer string) { 255 | // We need ASCII drawing characters; we need a line after the header; 256 | // *do* need a header! Do not need to markdown-escape contents of 257 | // tables as markdown is ignored in there. Do need to do _something_ 258 | // with a '|' character shown as a member of a table. 259 | 260 | t.Style.setAsciiBoxStyle() 261 | 262 | firstLines := make([]Element, 0, 2) 263 | 264 | if t.headers == nil { 265 | initial := createRenderStyle(t) 266 | if initial.columns > 1 { 267 | row := CreateRow([]interface{}{}) 268 | for i := 0; i < initial.columns; i++ { 269 | row.AddCell(CreateCell(i+1, &CellStyle{})) 270 | } 271 | } 272 | } 273 | 274 | firstLines = append(firstLines, CreateRow(t.headers)) 275 | // this is a dummy line, swapped out below: 276 | firstLines = append(firstLines, firstLines[0]) 277 | t.elements = append(firstLines, t.elements...) 278 | // generate the runtime style 279 | style := createRenderStyle(t) 280 | // we know that the second line is a dummy, we can replace it 281 | mdRow := CreateRow([]interface{}{}) 282 | for i := 0; i < style.columns; i++ { 283 | mdRow.AddCell(CreateCell(strings.Repeat("-", style.cellWidths[i]), &CellStyle{})) 284 | } 285 | t.elements[1] = mdRow 286 | 287 | // comes after style is generated, which must come after all 288 | // width-affecting changes are in 289 | if t.title != nil { 290 | // markdown doesn't support titles or column spanning; we _should_ 291 | // escape the title, but doing that to handle all possible forms of 292 | // markup would require a heavy dependency, so we punt. 293 | buffer += "Table: " + 294 | strings.TrimSpace(CreateCell(t.title, &CellStyle{}).Render(style)) + 295 | "\n\n" 296 | } 297 | 298 | // loop over the elements and render them 299 | for _, e := range t.elements { 300 | buffer += e.Render(style) + "\n" 301 | } 302 | 303 | return buffer 304 | } 305 | -------------------------------------------------------------------------------- /table_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012-2013 Apcera Inc. All rights reserved. 2 | package tablewriter 3 | 4 | import ( 5 | "testing" 6 | ) 7 | 8 | func DisplayFailedOutput(actual, expected string) string { 9 | return "Output didn't match expected\n\n" + 10 | "Actual:\n\n" + 11 | actual + "\n" + 12 | "Expected:\n\n" + 13 | expected 14 | } 15 | 16 | func checkRendersTo(t *testing.T, table *Table, expected string) { 17 | output := table.Render() 18 | if output != expected { 19 | t.Fatal(DisplayFailedOutput(output, expected)) 20 | } 21 | } 22 | 23 | func TestCreateTable(t *testing.T) { 24 | expected := "" + 25 | "+-----------+-------+\n" + 26 | "| Name | Value |\n" + 27 | "+-----------+-------+\n" + 28 | "| hey | you |\n" + 29 | "| ken | 1234 |\n" + 30 | "| derek | 3.14 |\n" + 31 | "| derek too | 3.15 |\n" + 32 | "| escaping | rox%% |\n" + 33 | "+-----------+-------+\n" 34 | 35 | table := CreateTable() 36 | 37 | table.AddHeaders("Name", "Value") 38 | table.AddRow("hey", "you") 39 | table.AddRow("ken", 1234) 40 | table.AddRow("derek", 3.14) 41 | table.AddRow("derek too", 3.1456788) 42 | table.AddRow("escaping", "rox%%") 43 | 44 | checkRendersTo(t, table, expected) 45 | } 46 | 47 | func TestStyleResets(t *testing.T) { 48 | expected := "" + 49 | "+-----------+-------+\n" + 50 | "| Name | Value |\n" + 51 | "+-----------+-------+\n" + 52 | "| hey | you |\n" + 53 | "| ken | 1234 |\n" + 54 | "| derek | 3.14 |\n" + 55 | "| derek too | 3.15 |\n" + 56 | "+-----------+-------+\n" 57 | 58 | table := CreateTable() 59 | table.UTF8Box() 60 | table.Style.setAsciiBoxStyle() 61 | 62 | table.AddHeaders("Name", "Value") 63 | table.AddRow("hey", "you") 64 | table.AddRow("ken", 1234) 65 | table.AddRow("derek", 3.14) 66 | table.AddRow("derek too", 3.1456788) 67 | 68 | checkRendersTo(t, table, expected) 69 | } 70 | 71 | func TestTableWithHeader(t *testing.T) { 72 | expected := "" + 73 | "+-------------------+\n" + 74 | "| Example |\n" + 75 | "+-----------+-------+\n" + 76 | "| Name | Value |\n" + 77 | "+-----------+-------+\n" + 78 | "| hey | you |\n" + 79 | "| ken | 1234 |\n" + 80 | "| derek | 3.14 |\n" + 81 | "| derek too | 3.15 |\n" + 82 | "+-----------+-------+\n" 83 | 84 | table := CreateTable() 85 | 86 | table.AddTitle("Example") 87 | table.AddHeaders("Name", "Value") 88 | table.AddRow("hey", "you") 89 | table.AddRow("ken", 1234) 90 | table.AddRow("derek", 3.14) 91 | table.AddRow("derek too", 3.1456788) 92 | 93 | checkRendersTo(t, table, expected) 94 | } 95 | 96 | func TestTableTitleWidthAdjusts(t *testing.T) { 97 | expected := "" + 98 | "+---------------------------+\n" + 99 | "| Example My Foo Bar'd Test |\n" + 100 | "+-----------+---------------+\n" + 101 | "| Name | Value |\n" + 102 | "+-----------+---------------+\n" + 103 | "| hey | you |\n" + 104 | "| ken | 1234 |\n" + 105 | "| derek | 3.14 |\n" + 106 | "| derek too | 3.15 |\n" + 107 | "+-----------+---------------+\n" 108 | 109 | table := CreateTable() 110 | 111 | table.AddTitle("Example My Foo Bar'd Test") 112 | table.AddHeaders("Name", "Value") 113 | table.AddRow("hey", "you") 114 | table.AddRow("ken", 1234) 115 | table.AddRow("derek", 3.14) 116 | table.AddRow("derek too", 3.1456788) 117 | 118 | checkRendersTo(t, table, expected) 119 | } 120 | 121 | func TestTableHeaderWidthAdjusts(t *testing.T) { 122 | expected := "" + 123 | "+---------------+---------------------+\n" + 124 | "| Slightly Long | More than 2 columns |\n" + 125 | "+---------------+---------------------+\n" + 126 | "| a | b |\n" + 127 | "+---------------+---------------------+\n" 128 | 129 | table := CreateTable() 130 | 131 | table.AddHeaders("Slightly Long", "More than 2 columns") 132 | table.AddRow("a", "b") 133 | 134 | checkRendersTo(t, table, expected) 135 | } 136 | 137 | func TestTableWithNoHeaders(t *testing.T) { 138 | expected := "" + 139 | "+-----------+------+\n" + 140 | "| hey | you |\n" + 141 | "| ken | 1234 |\n" + 142 | "| derek | 3.14 |\n" + 143 | "| derek too | 3.15 |\n" + 144 | "+-----------+------+\n" 145 | 146 | table := CreateTable() 147 | 148 | table.AddRow("hey", "you") 149 | table.AddRow("ken", 1234) 150 | table.AddRow("derek", 3.14) 151 | table.AddRow("derek too", 3.1456788) 152 | 153 | checkRendersTo(t, table, expected) 154 | } 155 | 156 | func TestTableUnicodeWidths(t *testing.T) { 157 | expected := "" + 158 | "+-----------+------+\n" + 159 | "| Name | Cost |\n" + 160 | "+-----------+------+\n" + 161 | "| Currency | ¤10 |\n" + 162 | "| US Dollar | $30 |\n" + 163 | "| Euro | €27 |\n" + 164 | "| Thai | ฿70 |\n" + 165 | "+-----------+------+\n" 166 | 167 | table := CreateTable() 168 | table.AddHeaders("Name", "Cost") 169 | table.AddRow("Currency", "¤10") 170 | table.AddRow("US Dollar", "$30") 171 | table.AddRow("Euro", "€27") 172 | table.AddRow("Thai", "฿70") 173 | 174 | checkRendersTo(t, table, expected) 175 | } 176 | 177 | func TestTableInUTF8(t *testing.T) { 178 | expected := "" + 179 | "╭───────────────────╮\n" + 180 | "│ Example │\n" + 181 | "├───────────┬───────┤\n" + 182 | "│ Name │ Value │\n" + 183 | "├───────────┼───────┤\n" + 184 | "│ hey │ you │\n" + 185 | "│ ken │ 1234 │\n" + 186 | "│ derek │ 3.14 │\n" + 187 | "│ derek too │ 3.15 │\n" + 188 | "│ escaping │ rox%% │\n" + 189 | "╰───────────┴───────╯\n" 190 | 191 | table := CreateTable() 192 | table.UTF8Box() 193 | 194 | table.AddTitle("Example") 195 | table.AddHeaders("Name", "Value") 196 | table.AddRow("hey", "you") 197 | table.AddRow("ken", 1234) 198 | table.AddRow("derek", 3.14) 199 | table.AddRow("derek too", 3.1456788) 200 | table.AddRow("escaping", "rox%%") 201 | 202 | checkRendersTo(t, table, expected) 203 | } 204 | 205 | func TestTableUnicodeUTF8AndSGR(t *testing.T) { 206 | // at present, this mostly just tests that alignment still works 207 | expected := "" + 208 | "╭───────────────────────╮\n" + 209 | "│ \033[1mFanciness\033[0m │\n" + 210 | "├──────────┬────────────┤\n" + 211 | "│ \033[31mred\033[0m │ \033[32mgreen\033[0m │\n" + 212 | "├──────────┼────────────┤\n" + 213 | "│ plain │ text │\n" + 214 | "│ Καλημέρα │ κόσμε │\n" + 215 | "│ \033[1mvery\033[0m │ \033[4munderlined\033[0m │\n" + 216 | "│ a\033[1mb\033[0mc │ \033[45mmagenta\033[0m │\n" + 217 | "│ \033[31m→\033[0m │ \033[32m←\033[0m │\n" + 218 | "╰──────────┴────────────╯\n" 219 | 220 | sgred := func(in string, sgrPm string) string { 221 | return "\033[" + sgrPm + "m" + in + "\033[0m" 222 | } 223 | bold := func(in string) string { return sgred(in, "1") } 224 | 225 | table := CreateTable() 226 | table.UTF8Box() 227 | 228 | table.AddTitle(bold("Fanciness")) 229 | table.AddHeaders(sgred("red", "31"), sgred("green", "32")) 230 | table.AddRow("plain", "text") 231 | table.AddRow("Καλημέρα", "κόσμε") // from http://plan9.bell-labs.com/sys/doc/utf.html 232 | table.AddRow(bold("very"), sgred("underlined", "4")) 233 | table.AddRow("a"+bold("b")+"c", sgred("magenta", "45")) 234 | table.AddRow(sgred("→", "31"), sgred("←", "32")) 235 | // TODO: in future, if we start detecting presence of SGR sequences, we 236 | // should ensure that the SGR reset is done at the end of the cell content, 237 | // so that SGR doesn't "bleed across" (cells or rows). We would then add 238 | // tests for that here. 239 | // 240 | // Of course, at that point, we'd also want to support automatic HTML 241 | // styling conversion too, so would need a test for that also. 242 | 243 | checkRendersTo(t, table, expected) 244 | } 245 | 246 | func TestTableInMarkdown(t *testing.T) { 247 | expected := "" + 248 | "Table: Example\n\n" + 249 | "| Name | Value |\n" + 250 | "| ----- | ----- |\n" + 251 | "| hey | you |\n" + 252 | "| a | b | esc |\n" + 253 | "| esc | rox%% |\n" 254 | 255 | table := CreateTable() 256 | table.SetModeMarkdown() 257 | 258 | table.AddTitle("Example") 259 | table.AddHeaders("Name", "Value") 260 | table.AddRow("hey", "you") 261 | table.AddRow("a | b", "esc") 262 | table.AddRow("esc", "rox%%") 263 | 264 | checkRendersTo(t, table, expected) 265 | } 266 | 267 | func TestTitleUnicodeWidths(t *testing.T) { 268 | expected := "" + 269 | "+-------+\n" + 270 | "| ← 5 → |\n" + 271 | "+---+---+\n" + 272 | "| a | b |\n" + 273 | "| c | d |\n" + 274 | "| e | 3 |\n" + 275 | "+---+---+\n" 276 | 277 | // minimum width for a table of two columns is 9 characters, given 278 | // one space of padding, and non-empty tables. 279 | 280 | table := CreateTable() 281 | 282 | // We have 4 characters down for left and right columns and padding, so 283 | // a width of 5 for us should match the minimum per the columns 284 | 285 | // 5 characters; each arrow is three octets in UTF-8, giving 9 bytes 286 | // so, same in character-count-width, longer in bytes 287 | table.AddTitle("← 5 →") 288 | 289 | // a single character per cell, here; use ASCII characters 290 | table.AddRow("a", "b") 291 | table.AddRow("c", "d") 292 | table.AddRow("e", 3) 293 | 294 | checkRendersTo(t, table, expected) 295 | } 296 | 297 | // We identified two error conditions wherein length wrapping would not correctly 298 | // wrap width when, for instance, in a two-column table, the longest row in the 299 | // right-hand column was not the same as the longest row in the left-hand column. 300 | // This tests that we correctly accumulate the maximum width across all rows of 301 | // the termtable and adjust width accordingly. 302 | func TestTableWidthHandling(t *testing.T) { 303 | expected := "" + 304 | "+-----------------------------------------+\n" + 305 | "| Example... to Fix My Test |\n" + 306 | "+-----------------+-----------------------+\n" + 307 | "| hey foo bar baz | you |\n" + 308 | "| ken | you should write code |\n" + 309 | "| derek | 3.14 |\n" + 310 | "| derek too | 3.15 |\n" + 311 | "+-----------------+-----------------------+\n" 312 | 313 | table := CreateTable() 314 | 315 | table.AddTitle("Example... to Fix My Test") 316 | table.AddRow("hey foo bar baz", "you") 317 | table.AddRow("ken", "you should write code") 318 | table.AddRow("derek", 3.14) 319 | table.AddRow("derek too", 3.1456788) 320 | 321 | output := table.Render() 322 | if output != expected { 323 | t.Fatal(DisplayFailedOutput(output, expected)) 324 | } 325 | 326 | } 327 | 328 | func TestTableWidthHandling_SecondErrorCondition(t *testing.T) { 329 | expected := "" + 330 | "+----------------------------------------+\n" + 331 | "| Example... to Fix My Test |\n" + 332 | "+-----------------+----------------------+\n" + 333 | "| hey foo bar baz | you |\n" + 334 | "| ken | you should sell cod! |\n" + 335 | "| derek | 3.14 |\n" + 336 | "| derek too | 3.15 |\n" + 337 | "+-----------------+----------------------+\n" 338 | 339 | table := CreateTable() 340 | 341 | table.AddTitle("Example... to Fix My Test") 342 | table.AddRow("hey foo bar baz", "you") 343 | table.AddRow("ken", "you should sell cod!") 344 | table.AddRow("derek", 3.14) 345 | table.AddRow("derek too", 3.1456788) 346 | 347 | output := table.Render() 348 | if output != expected { 349 | t.Fatal(DisplayFailedOutput(output, expected)) 350 | } 351 | } 352 | 353 | func TestTableAlignPostsetting(t *testing.T) { 354 | expected := "" + 355 | "+-----------+-------+\n" + 356 | "| Name | Value |\n" + 357 | "+-----------+-------+\n" + 358 | "| hey | you |\n" + 359 | "| ken | 1234 |\n" + 360 | "| derek | 3.14 |\n" + 361 | "| derek too | 3.15 |\n" + 362 | "| escaping | rox%% |\n" + 363 | "+-----------+-------+\n" 364 | 365 | table := CreateTable() 366 | 367 | table.AddHeaders("Name", "Value") 368 | table.AddRow("hey", "you") 369 | table.AddRow("ken", 1234) 370 | table.AddRow("derek", 3.14) 371 | table.AddRow("derek too", 3.1456788) 372 | table.AddRow("escaping", "rox%%") 373 | 374 | table.SetAlign(AlignRight, 1) 375 | 376 | checkRendersTo(t, table, expected) 377 | } 378 | 379 | func TestTableMissingCells(t *testing.T) { 380 | expected := "" + 381 | "+----------+---------+---------+\n" + 382 | "| Name | Value 1 | Value 2 |\n" + 383 | "+----------+---------+---------+\n" + 384 | "| hey | you | person |\n" + 385 | "| ken | 1234 |\n" + 386 | "| escaping | rox%s%% |\n" + 387 | "+----------+---------+---------+\n" 388 | // FIXME: missing extra cells there 389 | 390 | table := CreateTable() 391 | 392 | table.AddHeaders("Name", "Value 1", "Value 2") 393 | table.AddRow("hey", "you", "person") 394 | table.AddRow("ken", 1234) 395 | table.AddRow("escaping", "rox%s%%") 396 | 397 | checkRendersTo(t, table, expected) 398 | } 399 | 400 | // We don't yet support combining characters, double-width characters or 401 | // anything to do with estimating a tty-style "character width" for what in 402 | // Unicode is a grapheme cluster. This disabled test shows what we want 403 | // to support, but don't yet. 404 | func TestTableWithCombiningChars(t *testing.T) { 405 | t.Skip("FIXME: not implemented: grapheme cluster support & combining characters") 406 | expected := "" + 407 | "+------+---+\n" + 408 | "| noel | 1 |\n" + 409 | "| noël | 2 |\n" + 410 | "| noël | 3 |\n" + 411 | "+------+---+\n" 412 | 413 | table := CreateTable() 414 | 415 | table.AddRow("noel", "1") 416 | table.AddRow("noe\u0308l", "2") // LATIN SMALL LETTER E + COMBINING DIAERESIS 417 | table.AddRow("noël", "3") // Hex EB; LATIN SMALL LETTER E WITH DIAERESIS 418 | 419 | checkRendersTo(t, table, expected) 420 | } 421 | 422 | // another unicode length issue 423 | func TestTableWithFullwidthChars(t *testing.T) { 424 | t.Skip("FIXME: not implemented: grapheme cluster support & widechars") 425 | expected := "" + 426 | "+----------+------------+\n" + 427 | "| wide | not really |\n" + 428 | "| wide | fullwidth |\n" + 429 | "+----------+------------+\n" 430 | 431 | table := CreateTable() 432 | table.AddRow("wide", "not really") 433 | table.AddRow("wide", "fullwidth") // FULLWIDTH LATIN SMALL LETTER 434 | 435 | checkRendersTo(t, table, expected) 436 | } 437 | --------------------------------------------------------------------------------