From f28f400d7ebce09e4be1eabe87086e66d2947682 Mon Sep 17 00:00:00 2001 From: Niklas Fasching Date: Fri, 14 Dec 2018 15:49:40 +0100 Subject: [PATCH] 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... --- README.org | 2 - etc/style.css | 4 ++ org/html.go | 57 ++++++++-------- org/org.go | 73 ++++++++++++--------- org/table.go | 126 +++++++++++++++++++++++++----------- org/testdata/footnotes.html | 9 ++- org/testdata/lists.html | 10 ++- org/testdata/misc.html | 6 +- org/testdata/tables.html | 76 ++++++++++++++++++++-- org/testdata/tables.org | 11 ++++ 10 files changed, 261 insertions(+), 113 deletions(-) diff --git a/README.org b/README.org index 1799f99..d03d3ec 100644 --- a/README.org +++ b/README.org @@ -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 diff --git a/etc/style.css b/etc/style.css index cc9d896..285816b 100644 --- a/etc/style.css +++ b/etc/style.css @@ -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; } diff --git a/org/html.go b/org/html.go index 575cde3..30d6570 100644 --- a/org/html.go +++ b/org/html.go @@ -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("\n") - w.writeNodes(t.Header) - w.WriteString("\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("\n") + w.writeTableColumns(row.Columns, "th") + w.WriteString("\n\n") + continue + } else { + w.WriteString("\n") + } + } + w.writeTableColumns(row.Columns, "td") + } w.WriteString("\n
\n") } -func (w *HTMLWriter) writeTableRow(t TableRow) { +func (w *HTMLWriter) writeTableColumns(columns []Column, tag string) { w.WriteString("\n") - for _, column := range t.Columns { - w.WriteString("") - w.writeNodes(column...) - w.WriteString("") + 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("\n", tag)) } - w.WriteString("\n\n") -} - -func (w *HTMLWriter) writeTableHeader(t TableHeader) { - w.WriteString("\n") - for _, column := range t.Columns { - w.WriteString("") - w.writeNodes(column...) - w.WriteString("") - } - w.WriteString("\n\n") -} - -func (w *HTMLWriter) writeTableSeparator(t TableSeparator) { - w.WriteString("\n") + w.WriteString("\n") } func withHTMLAttributes(input string, kvs ...string) string { diff --git a/org/org.go b/org/org.go index d715f79..841fe77 100644 --- a/org/org.go +++ b/org/org.go @@ -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) { diff --git a/org/table.go b/org/table.go index ccd282d..30e94f8 100644 --- a/org/table.go +++ b/org/table.go @@ -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 } diff --git a/org/testdata/footnotes.html b/org/testdata/footnotes.html index c1eac09..78f9668 100644 --- a/org/testdata/footnotes.html +++ b/org/testdata/footnotes.html @@ -53,13 +53,16 @@ and tables - + + - + + - + +
1a1a
2b2b
3c3c
diff --git a/org/testdata/lists.html b/org/testdata/lists.html index c50f03f..bae58c4 100644 --- a/org/testdata/lists.html +++ b/org/testdata/lists.html @@ -60,11 +60,17 @@ and another one with a table

- + + + + + - + + +
abc
abc
123123
diff --git a/org/testdata/misc.html b/org/testdata/misc.html index 7542dd0..cabceb8 100644 --- a/org/testdata/misc.html +++ b/org/testdata/misc.html @@ -20,10 +20,12 @@ crazy ain't it? - + + - + +
foofoofoofoo
barbarbarbar
diff --git a/org/testdata/tables.html b/org/testdata/tables.html index fe5843b..eee44cf 100644 --- a/org/testdata/tables.html +++ b/org/testdata/tables.html @@ -1,11 +1,17 @@
- + + + + + - + + +
abc
abc
123123
@@ -16,11 +22,17 @@ table with separator before and after header
- + + + + + - + + +
abc
abc
123123
@@ -31,9 +43,10 @@ table with separator after header
- - + + +
123123
@@ -45,7 +58,9 @@ table without header (but separator before) - + + +
123123
@@ -53,3 +68,50 @@ table without header (but separator before) table without header
+
+ + + + + + + + + + + + + + + + + + + + +
left alignedright alignedcenter aligned
424242
foobarfoobarfoobar
+
+table with aligned columns +
+
+
+ + + + + + + + + + + + + + + +
long column along column blong column c
123
+
+table with right aligned columns (because numbers) +
+
diff --git a/org/testdata/tables.org b/org/testdata/tables.org index 56c3fbd..c2ee86f 100644 --- a/org/testdata/tables.org +++ b/org/testdata/tables.org @@ -16,3 +16,14 @@ #+CAPTION: table without header | 1 | 2 | 3 | +#+CAPTION: table with aligned columns +| left aligned | right aligned | center aligned | +|--------------+---------------+----------------| +| | | | +| 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 |