Add table pretty printing & alignment

Also dismissed implementing colgroups for now - had it but didn't like the
added complexity for a very questionable benefit - i've actually never used
that feature of org tables...
This commit is contained in:
Niklas Fasching 2018-12-14 15:49:40 +01:00
parent c08119bbc8
commit f28f400d7e
10 changed files with 261 additions and 113 deletions

View file

@ -8,8 +8,6 @@ A basic org-mode parser in go
- support more keywords: https://orgmode.org/manual/In_002dbuffer-settings.html
- #+LINK
- #+INCLUDE
- table colgroups https://orgmode.org/worg/org-tutorials/tables.html
- table pretty printing
*** TODO [[https://github.com/chaseadamsio/goorgeous/issues/10][#10]]: Support noexport
*** TODO [[https://github.com/chaseadamsio/goorgeous/issues/31][#31]]: Support #+INCLUDE
- see https://orgmode.org/manual/Include-files.html

View file

@ -104,3 +104,7 @@ figcaption {
background-color: #ccc; }
.footnote-definition .footnote-body p:only-child {
margin: 0.2em 0; }
.align-left { text-align: left; }
.align-center { text-align: center; }
.align-right { text-align: right; }

View file

@ -84,12 +84,6 @@ func (w *HTMLWriter) writeNodes(ns ...Node) {
case Table:
w.writeTable(n)
case TableHeader:
w.writeTableHeader(n)
case TableRow:
w.writeTableRow(n)
case TableSeparator:
w.writeTableSeparator(n)
case Paragraph:
w.writeParagraph(n)
@ -283,34 +277,39 @@ func (w *HTMLWriter) writeNodeWithMeta(n NodeWithMeta) {
func (w *HTMLWriter) writeTable(t Table) {
w.WriteString("<table>\n")
w.writeNodes(t.Header)
w.WriteString("<tbody>\n")
w.writeNodes(t.Rows...)
beforeFirstContentRow := true
for i, row := range t.Rows {
if row.IsSpecial || len(row.Columns) == 0 {
continue
}
if beforeFirstContentRow {
beforeFirstContentRow = false
if i+1 < len(t.Rows) && len(t.Rows[i+1].Columns) == 0 {
w.WriteString("<thead>\n")
w.writeTableColumns(row.Columns, "th")
w.WriteString("</thead>\n<tbody>\n")
continue
} else {
w.WriteString("<tbody>\n")
}
}
w.writeTableColumns(row.Columns, "td")
}
w.WriteString("</tbody>\n</table>\n")
}
func (w *HTMLWriter) writeTableRow(t TableRow) {
func (w *HTMLWriter) writeTableColumns(columns []Column, tag string) {
w.WriteString("<tr>\n")
for _, column := range t.Columns {
w.WriteString("<td>")
w.writeNodes(column...)
w.WriteString("</td>")
for _, column := range columns {
if column.Align == "" {
w.WriteString(fmt.Sprintf("<%s>", tag))
} else {
w.WriteString(fmt.Sprintf(`<%s class="align-%s">`, tag, column.Align))
}
w.writeNodes(column.Children...)
w.WriteString(fmt.Sprintf("</%s>\n", tag))
}
w.WriteString("\n</tr>\n")
}
func (w *HTMLWriter) writeTableHeader(t TableHeader) {
w.WriteString("<thead>\n")
for _, column := range t.Columns {
w.WriteString("<th>")
w.writeNodes(column...)
w.WriteString("</th>")
}
w.WriteString("\n</thead>\n")
}
func (w *HTMLWriter) writeTableSeparator(t TableSeparator) {
w.WriteString("<tr></tr>\n")
w.WriteString("</tr>\n")
}
func withHTMLAttributes(input string, kvs ...string) string {

View file

@ -41,6 +41,12 @@ func (w *OrgWriter) emptyClone() *OrgWriter {
return &wcopy
}
func (w *OrgWriter) nodesAsString(nodes ...Node) string {
tmp := w.emptyClone()
tmp.writeNodes(nodes...)
return tmp.String()
}
func (w *OrgWriter) writeNodes(ns ...Node) {
for _, n := range ns {
switch n := n.(type) {
@ -65,12 +71,6 @@ func (w *OrgWriter) writeNodes(ns ...Node) {
case Table:
w.writeTable(n)
case TableHeader:
w.writeTableHeader(n)
case TableRow:
w.writeTableRow(n)
case TableSeparator:
w.writeTableSeparator(n)
case Paragraph:
w.writeParagraph(n)
@ -206,34 +206,45 @@ func (w *OrgWriter) writeListItem(li ListItem) {
}
func (w *OrgWriter) writeTable(t Table) {
w.writeNodes(t.Header)
w.writeNodes(t.Rows...)
}
for _, row := range t.Rows {
w.WriteString(w.indent)
if len(row.Columns) == 0 {
w.WriteString(`|`)
for i := 0; i < len(t.ColumnInfos); i++ {
w.WriteString(strings.Repeat("-", t.ColumnInfos[i].Len+2))
if i < len(t.ColumnInfos)-1 {
w.WriteString("+")
}
}
w.WriteString(`|`)
func (w *OrgWriter) writeTableHeader(th TableHeader) {
w.writeNodes(th.SeparatorBefore)
w.writeTableColumns(th.Columns)
w.writeNodes(th.SeparatorAfter)
}
func (w *OrgWriter) writeTableRow(tr TableRow) {
w.writeTableColumns(tr.Columns)
}
func (w *OrgWriter) writeTableSeparator(ts TableSeparator) {
w.WriteString(w.indent + ts.Content + "\n")
}
func (w *OrgWriter) writeTableColumns(columns [][]Node) {
w.WriteString(w.indent + "| ")
for i, columnNodes := range columns {
w.writeNodes(columnNodes...)
w.WriteString(" |")
if i < len(columns)-1 {
w.WriteString(" ")
} else {
w.WriteString(`|`)
for _, column := range row.Columns {
w.WriteString(` `)
content := w.nodesAsString(column.Children...)
if content == "" {
content = " "
}
n := column.Len - len(content)
if n < 0 {
n = 0
}
if column.Align == "center" {
if n%2 != 0 {
w.WriteString(" ")
}
w.WriteString(strings.Repeat(" ", n/2) + content + strings.Repeat(" ", n/2))
} else if column.Align == "right" {
w.WriteString(strings.Repeat(" ", n) + content)
} else {
w.WriteString(content + strings.Repeat(" ", n))
}
w.WriteString(` |`)
}
}
w.WriteString("\n")
}
w.WriteString("\n")
}
func (w *OrgWriter) writeHorizontalRule(hr HorizontalRule) {

View file

@ -2,27 +2,35 @@ package org
import (
"regexp"
"strconv"
"strings"
)
type Table struct {
Header Node
Rows []Node
Rows []Row
ColumnInfos []ColumnInfo
}
type TableSeparator struct{ Content string }
type TableHeader struct {
SeparatorBefore Node
Columns [][]Node
SeparatorAfter Node
type Row struct {
Columns []Column
IsSpecial bool
}
type TableRow struct{ Columns [][]Node }
type Column struct {
Children []Node
*ColumnInfo
}
type ColumnInfo struct {
Align string
Len int
}
var tableSeparatorRegexp = regexp.MustCompile(`^(\s*)(\|[+-|]*)\s*$`)
var tableRowRegexp = regexp.MustCompile(`^(\s*)(\|.*)`)
var columnAlignRegexp = regexp.MustCompile(`^<(l|c|r)>$`)
func lexTable(line string) (token, bool) {
if m := tableSeparatorRegexp.FindStringSubmatch(line); m != nil {
return token{"tableSeparator", len(m[1]), m[2], m}, true
@ -33,43 +41,87 @@ func lexTable(line string) (token, bool) {
}
func (d *Document) parseTable(i int, parentStop stopFn) (int, Node) {
rows, start := []Node{}, i
for !parentStop(d, i) && (d.tokens[i].kind == "tableRow" || d.tokens[i].kind == "tableSeparator") {
consumed, row := d.parseTableRowOrSeparator(i, parentStop)
i += consumed
rows = append(rows, row)
}
consumed := i - start
if len(rows) >= 2 {
if row, ok := rows[0].(TableRow); ok {
if separator, ok := rows[1].(TableSeparator); ok {
return consumed, Table{TableHeader{nil, row.Columns, separator}, rows[2:]}
rawRows, start := [][]string{}, i
for ; !parentStop(d, i); i++ {
if t := d.tokens[i]; t.kind == "tableRow" {
rawRow := strings.FieldsFunc(d.tokens[i].content, func(r rune) bool { return r == '|' })
for i := range rawRow {
rawRow[i] = strings.TrimSpace(rawRow[i])
}
rawRows = append(rawRows, rawRow)
} else if t.kind == "tableSeparator" {
rawRows = append(rawRows, nil)
} else {
break
}
}
if len(rows) >= 3 {
if separatorBefore, ok := rows[0].(TableSeparator); ok {
if row, ok := rows[1].(TableRow); ok {
if separatorAfter, ok := rows[2].(TableSeparator); ok {
return consumed, Table{TableHeader{separatorBefore, row.Columns, separatorAfter}, rows[3:]}
table := Table{nil, getColumnInfos(rawRows)}
for _, rawColumns := range rawRows {
row := Row{nil, isSpecialRow(rawColumns)}
if len(rawColumns) != 0 {
for i := range table.ColumnInfos {
column := Column{nil, &table.ColumnInfos[i]}
if i < len(rawColumns) {
column.Children = d.parseInline(rawColumns[i])
}
row.Columns = append(row.Columns, column)
}
}
table.Rows = append(table.Rows, row)
}
return i - start, table
}
func getColumnInfos(rows [][]string) []ColumnInfo {
columnCount := 0
for _, columns := range rows {
if n := len(columns); n > columnCount {
columnCount = n
}
}
return consumed, Table{nil, rows}
columnInfos := make([]ColumnInfo, columnCount)
for i := 0; i < columnCount; i++ {
countNumeric, countNonNumeric := 0, 0
for _, columns := range rows {
if !(i < len(columns)) {
continue
}
if len(columns[i]) > columnInfos[i].Len {
columnInfos[i].Len = len(columns[i])
}
if m := columnAlignRegexp.FindStringSubmatch(columns[i]); m != nil && isSpecialRow(columns) {
switch m[1] {
case "l":
columnInfos[i].Align = "left"
case "c":
columnInfos[i].Align = "center"
case "r":
columnInfos[i].Align = "right"
}
} else if _, err := strconv.ParseFloat(columns[i], 32); err == nil {
countNumeric++
} else if strings.TrimSpace(columns[i]) != "" {
countNonNumeric++
}
}
if columnInfos[i].Align == "" && countNumeric >= countNonNumeric {
columnInfos[i].Align = "right"
}
}
return columnInfos
}
func (d *Document) parseTableRowOrSeparator(i int, _ stopFn) (int, Node) {
if d.tokens[i].kind == "tableSeparator" {
return 1, TableSeparator{d.tokens[i].content}
func isSpecialRow(rawColumns []string) bool {
isAlignRow := true
for _, rawColumn := range rawColumns {
if !columnAlignRegexp.MatchString(rawColumn) && rawColumn != "" {
isAlignRow = false
}
}
fields := strings.FieldsFunc(d.tokens[i].content, func(r rune) bool { return r == '|' })
row := TableRow{}
for _, field := range fields {
row.Columns = append(row.Columns, d.parseInline(strings.TrimSpace(field)))
}
return 1, row
return isAlignRow
}

View file

@ -53,13 +53,16 @@ and tables
<table>
<tbody>
<tr>
<td>1</td><td>a</td>
<td class="align-right">1</td>
<td>a</td>
</tr>
<tr>
<td>2</td><td>b</td>
<td class="align-right">2</td>
<td>b</td>
</tr>
<tr>
<td>3</td><td>c</td>
<td class="align-right">3</td>
<td>c</td>
</tr>
</tbody>
</table>

View file

@ -60,11 +60,17 @@ and another one with a table
</p>
<table>
<thead>
<th>a</th><th>b</th><th>c</th>
<tr>
<th class="align-right">a</th>
<th class="align-right">b</th>
<th class="align-right">c</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td><td>2</td><td>3</td>
<td class="align-right">1</td>
<td class="align-right">2</td>
<td class="align-right">3</td>
</tr>
</tbody>
</table>

View file

@ -20,10 +20,12 @@ crazy ain&#39;t it?
<table>
<tbody>
<tr>
<td><strong>foo</strong></td><td>foo</td>
<td><strong>foo</strong></td>
<td>foo</td>
</tr>
<tr>
<td><strong>bar</strong></td><td>bar</td>
<td><strong>bar</strong></td>
<td>bar</td>
</tr>
</tbody>
</table>

View file

@ -1,11 +1,17 @@
<figure>
<table>
<thead>
<th>a</th><th>b</th><th>c</th>
<tr>
<th class="align-right">a</th>
<th class="align-right">b</th>
<th class="align-right">c</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td><td>2</td><td>3</td>
<td class="align-right">1</td>
<td class="align-right">2</td>
<td class="align-right">3</td>
</tr>
</tbody>
</table>
@ -16,11 +22,17 @@ table with separator before and after header
<figure>
<table>
<thead>
<th>a</th><th>b</th><th>c</th>
<tr>
<th class="align-right">a</th>
<th class="align-right">b</th>
<th class="align-right">c</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td><td>2</td><td>3</td>
<td class="align-right">1</td>
<td class="align-right">2</td>
<td class="align-right">3</td>
</tr>
</tbody>
</table>
@ -31,9 +43,10 @@ table with separator after header
<figure>
<table>
<tbody>
<tr></tr>
<tr>
<td>1</td><td>2</td><td>3</td>
<td class="align-right">1</td>
<td class="align-right">2</td>
<td class="align-right">3</td>
</tr>
</tbody>
</table>
@ -45,7 +58,9 @@ table without header (but separator before)
<table>
<tbody>
<tr>
<td>1</td><td>2</td><td>3</td>
<td class="align-right">1</td>
<td class="align-right">2</td>
<td class="align-right">3</td>
</tr>
</tbody>
</table>
@ -53,3 +68,50 @@ table without header (but separator before)
table without header
</figcaption>
</figure>
<figure>
<table>
<thead>
<tr>
<th class="align-left">left aligned</th>
<th class="align-right">right aligned</th>
<th class="align-center">center aligned</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-left">42</td>
<td class="align-right">42</td>
<td class="align-center">42</td>
</tr>
<tr>
<td class="align-left">foobar</td>
<td class="align-right">foobar</td>
<td class="align-center">foobar</td>
</tr>
</tbody>
</table>
<figcaption>
table with aligned columns
</figcaption>
</figure>
<figure>
<table>
<thead>
<tr>
<th class="align-right">long column a</th>
<th class="align-right">long column b</th>
<th class="align-right">long column c</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-right">1</td>
<td class="align-right">2</td>
<td class="align-right">3</td>
</tr>
</tbody>
</table>
<figcaption>
table with right aligned columns (because numbers)
</figcaption>
</figure>

View file

@ -16,3 +16,14 @@
#+CAPTION: table without header
| 1 | 2 | 3 |
#+CAPTION: table with aligned columns
| left aligned | right aligned | center aligned |
|--------------+---------------+----------------|
| <l> | <r> | <c> |
| 42 | 42 | 42 |
| foobar | foobar | foobar |
#+CAPTION: table with right aligned columns (because numbers)
| long column a | long column b | long column c |
|---------------+---------------+---------------|
| 1 | 2 | 3 |