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 - support more keywords: https://orgmode.org/manual/In_002dbuffer-settings.html
- #+LINK - #+LINK
- #+INCLUDE - #+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/10][#10]]: Support noexport
*** TODO [[https://github.com/chaseadamsio/goorgeous/issues/31][#31]]: Support #+INCLUDE *** TODO [[https://github.com/chaseadamsio/goorgeous/issues/31][#31]]: Support #+INCLUDE
- see https://orgmode.org/manual/Include-files.html - see https://orgmode.org/manual/Include-files.html

View file

@ -104,3 +104,7 @@ figcaption {
background-color: #ccc; } background-color: #ccc; }
.footnote-definition .footnote-body p:only-child { .footnote-definition .footnote-body p:only-child {
margin: 0.2em 0; } 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: case Table:
w.writeTable(n) w.writeTable(n)
case TableHeader:
w.writeTableHeader(n)
case TableRow:
w.writeTableRow(n)
case TableSeparator:
w.writeTableSeparator(n)
case Paragraph: case Paragraph:
w.writeParagraph(n) w.writeParagraph(n)
@ -283,34 +277,39 @@ func (w *HTMLWriter) writeNodeWithMeta(n NodeWithMeta) {
func (w *HTMLWriter) writeTable(t Table) { func (w *HTMLWriter) writeTable(t Table) {
w.WriteString("<table>\n") w.WriteString("<table>\n")
w.writeNodes(t.Header) beforeFirstContentRow := true
w.WriteString("<tbody>\n") for i, row := range t.Rows {
w.writeNodes(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") w.WriteString("</tbody>\n</table>\n")
} }
func (w *HTMLWriter) writeTableRow(t TableRow) { func (w *HTMLWriter) writeTableColumns(columns []Column, tag string) {
w.WriteString("<tr>\n") w.WriteString("<tr>\n")
for _, column := range t.Columns { for _, column := range columns {
w.WriteString("<td>") if column.Align == "" {
w.writeNodes(column...) w.WriteString(fmt.Sprintf("<%s>", tag))
w.WriteString("</td>") } 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") w.WriteString("</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")
} }
func withHTMLAttributes(input string, kvs ...string) string { func withHTMLAttributes(input string, kvs ...string) string {

View file

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

View file

@ -2,27 +2,35 @@ package org
import ( import (
"regexp" "regexp"
"strconv"
"strings" "strings"
) )
type Table struct { type Table struct {
Header Node Rows []Row
Rows []Node ColumnInfos []ColumnInfo
} }
type TableSeparator struct{ Content string } type Row struct {
Columns []Column
type TableHeader struct { IsSpecial bool
SeparatorBefore Node
Columns [][]Node
SeparatorAfter Node
} }
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 tableSeparatorRegexp = regexp.MustCompile(`^(\s*)(\|[+-|]*)\s*$`)
var tableRowRegexp = regexp.MustCompile(`^(\s*)(\|.*)`) var tableRowRegexp = regexp.MustCompile(`^(\s*)(\|.*)`)
var columnAlignRegexp = regexp.MustCompile(`^<(l|c|r)>$`)
func lexTable(line string) (token, bool) { func lexTable(line string) (token, bool) {
if m := tableSeparatorRegexp.FindStringSubmatch(line); m != nil { if m := tableSeparatorRegexp.FindStringSubmatch(line); m != nil {
return token{"tableSeparator", len(m[1]), m[2], m}, true 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) { func (d *Document) parseTable(i int, parentStop stopFn) (int, Node) {
rows, start := []Node{}, i rawRows, start := [][]string{}, i
for !parentStop(d, i) && (d.tokens[i].kind == "tableRow" || d.tokens[i].kind == "tableSeparator") { for ; !parentStop(d, i); i++ {
consumed, row := d.parseTableRowOrSeparator(i, parentStop) if t := d.tokens[i]; t.kind == "tableRow" {
i += consumed rawRow := strings.FieldsFunc(d.tokens[i].content, func(r rune) bool { return r == '|' })
rows = append(rows, row) for i := range rawRow {
} rawRow[i] = strings.TrimSpace(rawRow[i])
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 = 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 { table := Table{nil, getColumnInfos(rawRows)}
if row, ok := rows[1].(TableRow); ok { for _, rawColumns := range rawRows {
if separatorAfter, ok := rows[2].(TableSeparator); ok { row := Row{nil, isSpecialRow(rawColumns)}
return consumed, Table{TableHeader{separatorBefore, row.Columns, separatorAfter}, rows[3:]} 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) { func isSpecialRow(rawColumns []string) bool {
if d.tokens[i].kind == "tableSeparator" { isAlignRow := true
return 1, TableSeparator{d.tokens[i].content} for _, rawColumn := range rawColumns {
if !columnAlignRegexp.MatchString(rawColumn) && rawColumn != "" {
isAlignRow = false
}
} }
fields := strings.FieldsFunc(d.tokens[i].content, func(r rune) bool { return r == '|' }) return isAlignRow
row := TableRow{}
for _, field := range fields {
row.Columns = append(row.Columns, d.parseInline(strings.TrimSpace(field)))
}
return 1, row
} }

View file

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

View file

@ -60,11 +60,17 @@ and another one with a table
</p> </p>
<table> <table>
<thead> <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> </thead>
<tbody> <tbody>
<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> </tr>
</tbody> </tbody>
</table> </table>

View file

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

View file

@ -1,11 +1,17 @@
<figure> <figure>
<table> <table>
<thead> <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> </thead>
<tbody> <tbody>
<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> </tr>
</tbody> </tbody>
</table> </table>
@ -16,11 +22,17 @@ table with separator before and after header
<figure> <figure>
<table> <table>
<thead> <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> </thead>
<tbody> <tbody>
<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> </tr>
</tbody> </tbody>
</table> </table>
@ -31,9 +43,10 @@ table with separator after header
<figure> <figure>
<table> <table>
<tbody> <tbody>
<tr></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> </tr>
</tbody> </tbody>
</table> </table>
@ -45,7 +58,9 @@ table without header (but separator before)
<table> <table>
<tbody> <tbody>
<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> </tr>
</tbody> </tbody>
</table> </table>
@ -53,3 +68,50 @@ table without header (but separator before)
table without header table without header
</figcaption> </figcaption>
</figure> </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 #+CAPTION: table without header
| 1 | 2 | 3 | | 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 |