Skip to content

Commit

Permalink
Add support for multi-line headers and footers.
Browse files Browse the repository at this point in the history
Also add a new option "reflow during auto wrap"
(`SetReflowDuringAutoWrap()`), which when enabled merges all the lines
in a multi-line cell together before to wrapping, and when disabled
causes paragraphs in a multi-line cell to be cleanly separated.

The default is true to preserve the previous behavior in existing
code.
  • Loading branch information
knz committed Jan 22, 2018
1 parent 96aac99 commit a2f2500
Show file tree
Hide file tree
Showing 5 changed files with 402 additions and 136 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Generate ASCII table on the fly ... Installation is simple as
- Set custom footer support
- Optional identical cells merging
- Set custom caption

- Optional reflowing of paragrpahs in multi-line cells.

#### Example 1 - Basic
```go
Expand Down Expand Up @@ -272,8 +272,6 @@ Movie ratings.
- ~~Support for `SetFooter`~~ - `done`
- ~~Support for `SetBorder`~~ - `done`
- ~~Support table with uneven rows~~ - `done`
- Support custom alignment
- ~~Support custom alignment~~
- General Improvement & Optimisation
- `NewHTML` Parse table from HTML


237 changes: 139 additions & 98 deletions table.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ type Table struct {
lines [][][]string
cs map[int]int
rs map[int]int
headers []string
footers []string
headers [][]string
footers [][]string
caption bool
captionText string
autoFmt bool
autoWrap bool
reflowText bool
mW int
pCenter string
pRow string
Expand Down Expand Up @@ -89,12 +90,13 @@ func NewWriter(writer io.Writer) *Table {
lines: [][][]string{},
cs: make(map[int]int),
rs: make(map[int]int),
headers: []string{},
footers: []string{},
headers: [][]string{},
footers: [][]string{},
caption: false,
captionText: "Table caption.",
autoFmt: true,
autoWrap: true,
reflowText: true,
mW: MAX_ROW_WIDTH,
pCenter: CENTER,
pRow: ROW,
Expand Down Expand Up @@ -137,21 +139,26 @@ func (t *Table) Render() {
}
}

const (
headerRowIdx = -1
footerRowIdx = -2
)

// Set table header
func (t *Table) SetHeader(keys []string) {
t.colSize = len(keys)
for i, v := range keys {
t.parseDimension(v, i, -1)
t.headers = append(t.headers, v)
lines := t.parseDimension(v, i, headerRowIdx)
t.headers = append(t.headers, lines)
}
}

// Set table Footer
func (t *Table) SetFooter(keys []string) {
//t.colSize = len(keys)
for i, v := range keys {
t.parseDimension(v, i, -1)
t.footers = append(t.footers, v)
lines := t.parseDimension(v, i, footerRowIdx)
t.footers = append(t.footers, lines)
}
}

Expand All @@ -173,6 +180,11 @@ func (t *Table) SetAutoWrapText(auto bool) {
t.autoWrap = auto
}

// Turn automatic reflowing of multiline text when rewrapping. Default is on (true).
func (t *Table) SetReflowDuringAutoWrap(auto bool) {
t.reflowText = auto
}

// Set the Default column width
func (t *Table) SetColWidth(width int) {
t.mW = width
Expand Down Expand Up @@ -304,7 +316,7 @@ func (t *Table) ClearRows() {

// Clear footer
func (t *Table) ClearFooter() {
t.footers = []string{}
t.footers = [][]string{}
}

// Print line based on row width
Expand Down Expand Up @@ -367,10 +379,6 @@ func (t *Table) printHeading() {
return
}

// Check if border is set
// Replace with space if not set
fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE))

// Identify last column
end := len(t.cs) - 1

Expand All @@ -383,31 +391,39 @@ func (t *Table) printHeading() {
is_esc_seq = true
}

// Print Heading column
for i := 0; i <= end; i++ {
v := t.cs[i]
h := ""
if i < len(t.headers) {
h = t.headers[i]
}
if t.autoFmt {
h = Title(h)
}
pad := ConditionString((i == end && !t.borders.Left), SPACE, t.pColumn)
// Maximum height.
max := t.rs[headerRowIdx]

if is_esc_seq {
fmt.Fprintf(t.out, " %s %s",
format(padFunc(h, SPACE, v),
t.headerParams[i]), pad)
} else {
fmt.Fprintf(t.out, " %s %s",
padFunc(h, SPACE, v),
pad)
}
// Print Heading
for x := 0; x < max; x++ {
// Check if border is set
// Replace with space if not set
fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE))

for y := 0; y <= end; y++ {
v := t.cs[y]
h := ""
if y < len(t.headers) && x < len(t.headers[y]) {
h = t.headers[y][x]
}
if t.autoFmt {
h = Title(h)
}
pad := ConditionString((y == end && !t.borders.Left), SPACE, t.pColumn)

if is_esc_seq {
fmt.Fprintf(t.out, " %s %s",
format(padFunc(h, SPACE, v),
t.headerParams[y]), pad)
} else {
fmt.Fprintf(t.out, " %s %s",
padFunc(h, SPACE, v),
pad)
}
}
// Next line
fmt.Fprint(t.out, t.newLine)
}
// Next line
fmt.Fprint(t.out, t.newLine)
if t.hdrLine {
t.printLine(true)
}
Expand All @@ -424,9 +440,6 @@ func (t *Table) printFooter() {
if !t.borders.Bottom {
t.printLine(true)
}
// Check if border is set
// Replace with space if not set
fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE))

// Identify last column
end := len(t.cs) - 1
Expand All @@ -440,44 +453,58 @@ func (t *Table) printFooter() {
is_esc_seq = true
}

// Print Heading column
for i := 0; i <= end; i++ {
v := t.cs[i]
f := t.footers[i]
if t.autoFmt {
f = Title(f)
}
pad := ConditionString((i == end && !t.borders.Top), SPACE, t.pColumn)
// Maximum height.
max := t.rs[footerRowIdx]

if len(t.footers[i]) == 0 {
pad = SPACE
}
// Print Footer
erasePad := make([]bool, len(t.footers))
for x := 0; x < max; x++ {
// Check if border is set
// Replace with space if not set
fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE))

if is_esc_seq {
fmt.Fprintf(t.out, " %s %s",
format(padFunc(f, SPACE, v),
t.footerParams[i]), pad)
} else {
fmt.Fprintf(t.out, " %s %s",
padFunc(f, SPACE, v),
pad)
}
for y := 0; y <= end; y++ {
v := t.cs[y]
f := ""
if y < len(t.footers) && x < len(t.footers[y]) {
f = t.footers[y][x]
}
if t.autoFmt {
f = Title(f)
}
pad := ConditionString((y == end && !t.borders.Top), SPACE, t.pColumn)

if erasePad[y] || (x == 0 && len(f) == 0) {
pad = SPACE
erasePad[y] = true
}

//fmt.Fprintf(t.out, " %s %s",
// padFunc(f, SPACE, v),
// pad)
if is_esc_seq {
fmt.Fprintf(t.out, " %s %s",
format(padFunc(f, SPACE, v),
t.footerParams[y]), pad)
} else {
fmt.Fprintf(t.out, " %s %s",
padFunc(f, SPACE, v),
pad)
}

//fmt.Fprintf(t.out, " %s %s",
// padFunc(f, SPACE, v),
// pad)
}
// Next line
fmt.Fprint(t.out, t.newLine)
//t.printLine(true)
}
// Next line
fmt.Fprint(t.out, t.newLine)
//t.printLine(true)

hasPrinted := false

for i := 0; i <= end; i++ {
v := t.cs[i]
pad := t.pRow
center := t.pCenter
length := len(t.footers[i])
length := len(t.footers[i][0])

if length > 0 {
hasPrinted = true
Expand Down Expand Up @@ -505,7 +532,7 @@ func (t *Table) printFooter() {

// Change Center start position
if center == SPACE {
if i < end && len(t.footers[i+1]) != 0 {
if i < end && len(t.footers[i+1][0]) != 0 {
center = t.pCenter
}
}
Expand Down Expand Up @@ -564,9 +591,9 @@ func (t *Table) fillAlignment(num int) {
// Print Row Information
// Adjust column alignment based on type

func (t *Table) printRow(columns [][]string, colKey int) {
func (t *Table) printRow(columns [][]string, rowIdx int) {
// Get Maximum Height
max := t.rs[colKey]
max := t.rs[rowIdx]
total := len(columns)

// TODO Fix uneven col size
Expand All @@ -578,7 +605,6 @@ func (t *Table) printRow(columns [][]string, colKey int) {
//}

// Pad Each Height
// pads := []int{}
pads := []int{}

// Checking for ANSI escape sequences for columns
Expand Down Expand Up @@ -672,9 +698,9 @@ func (t *Table) printRowsMergeCells() {
// Print Row Information to a writer and merge identical cells.
// Adjust column alignment based on type

func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, colKey int, previousLine []string) ([]string, []bool) {
func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, rowIdx int, previousLine []string) ([]string, []bool) {
// Get Maximum Height
max := t.rs[colKey]
max := t.rs[rowIdx]
total := len(columns)

// Pad Each Height
Expand Down Expand Up @@ -749,44 +775,59 @@ func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, colKey

func (t *Table) parseDimension(str string, colKey, rowKey int) []string {
var (
raw []string
max int
raw []string
maxWidth int
)
w := DisplayWidth(str)
// Calculate Width
// Check if with is grater than maximum width
if w > t.mW {
w = t.mW
}

// Check if width exists
v, ok := t.cs[colKey]
if !ok || v < w || v == 0 {
t.cs[colKey] = w
raw = getLines(str)
maxWidth = 0
for _, line := range raw {
if w := DisplayWidth(line); w > maxWidth {
maxWidth = w
}
}

if rowKey == -1 {
return raw
}
// Calculate Height
// If wrapping, ensure that all paragraphs in the cell fit in the
// specified width.
if t.autoWrap {
raw, _ = WrapString(str, t.cs[colKey])
} else {
raw = getLines(str)
}
// If there's a maximum allowed width for wrapping, use that.
if maxWidth > t.mW {
maxWidth = t.mW
}

for _, line := range raw {
if w := DisplayWidth(line); w > max {
max = w
// In the process of doing so, we need to recompute maxWidth. This
// is because perhaps a word in the cell is longer than the
// allowed maximum width in t.mW.
newMaxWidth := maxWidth
newRaw := make([]string, 0, len(raw))

if t.reflowText {
// Make a single paragraph of everything.
raw = []string{strings.Join(raw, " ")}
}
for i, para := range raw {
paraLines, _ := WrapString(para, maxWidth)
for _, line := range paraLines {
if w := DisplayWidth(line); w > newMaxWidth {
newMaxWidth = w
}
}
if i > 0 {
newRaw = append(newRaw, " ")
}
newRaw = append(newRaw, paraLines...)
}
raw = newRaw
maxWidth = newMaxWidth
}

// Make sure the with is the same length as maximum word
// Important for cases where the width is smaller than maxu word
if max > t.cs[colKey] {
t.cs[colKey] = max
// Store the new known maximum width.
v, ok := t.cs[colKey]
if !ok || v < maxWidth || v == 0 {
t.cs[colKey] = maxWidth
}

// Remember the number of lines for the row printer.
h := len(raw)
v, ok = t.rs[rowKey]

Expand Down
Loading

0 comments on commit a2f2500

Please sign in to comment.