From e5d791165dfdce7026e782163e97c74e2eed057c Mon Sep 17 00:00:00 2001 From: Timur Demin Date: Mon, 9 Aug 2021 18:39:01 +0500 Subject: [PATCH] Implement tables rendering This adds support for Markdown table rendering with github.com/olekukonko/tablewriter. Tables are rendered as ASCII text in a preformatted text block. Cells are hard-aligned with spaces. tablewriter options are not yet configurable, although they should be. For now, extra formatting inside tables is omitted. Fixes #2. --- go.mod | 6 ++- go.sum | 16 +++++-- internal/gemini/renderer.go | 91 +++++++++++++++++++++++++++++++++---- 3 files changed, 99 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 77dadbb..59fe983 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/tdemin/gmnhg go 1.16 require ( - github.com/gomarkdown/markdown v0.0.0-20201024011455-45c732cc8a6b - gopkg.in/yaml.v2 v2.3.0 + github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7 + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/olekukonko/tablewriter v0.0.5 + gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 08d7486..4a47f72 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,14 @@ -github.com/gomarkdown/markdown v0.0.0-20201024011455-45c732cc8a6b h1:Om9FdD4lzIJELyJxwr9EWSjaG6GMUNS3iebnhrGevhI= -github.com/gomarkdown/markdown v0.0.0-20201024011455-45c732cc8a6b/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU= +github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7 h1:oKYOfNR7Hp6XpZ4JqolL5u642Js5Z0n7psPVl+S5heo= +github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/gemini/renderer.go b/internal/gemini/renderer.go index 81e23da..6c75610 100644 --- a/internal/gemini/renderer.go +++ b/internal/gemini/renderer.go @@ -25,15 +25,17 @@ import ( "time" "github.com/gomarkdown/markdown/ast" + "github.com/olekukonko/tablewriter" ) var ( - lineBreak = []byte{'\n'} - space = []byte{' '} - linkPrefix = []byte("=> ") - quotePrefix = []byte("> ") - itemPrefix = []byte("* ") - itemIndent = []byte{'\t'} + lineBreak = []byte{'\n'} + space = []byte{' '} + linkPrefix = []byte("=> ") + quotePrefix = []byte("> ") + itemPrefix = []byte("* ") + itemIndent = []byte{'\t'} + preformattedToggle = []byte("```\n") ) var meaningfulCharsRegex = regexp.MustCompile(`\A[\s]+\z`) @@ -193,9 +195,9 @@ func (r Renderer) paragraph(w io.Writer, node *ast.Paragraph, entering bool) (no } func (r Renderer) code(w io.Writer, node *ast.CodeBlock) { - w.Write([]byte("```\n")) + w.Write(preformattedToggle) w.Write(node.Literal) - w.Write([]byte("```\n")) + w.Write(preformattedToggle) } func (r Renderer) list(w io.Writer, node *ast.List, level int) { @@ -249,6 +251,76 @@ func (r Renderer) text(w io.Writer, node ast.Node) { } } +func extractText(node ast.Node) string { + if node := node.AsLeaf(); node != nil { + return strings.ReplaceAll(string(node.Literal), "\n", " ") + } + if node := node.AsContainer(); node != nil { + b := strings.Builder{} + for _, child := range node.Children { + b.WriteString(extractText(child)) + } + return b.String() + } + panic("encountered a non-leaf & non-container node") +} + +func (r Renderer) tableHead(t *tablewriter.Table, node *ast.TableHeader) { + if node := node.AsContainer(); node != nil { + // should always have a single row consisting of at least one + // cell but worth checking nonetheless; tablewriter only + // supports a single header row as of now therefore ignore + // second row and the rest + if len(node.Children) > 0 { + if row := node.Children[0].AsContainer(); row != nil { + cells := make([]string, len(row.Children)) + for i, cell := range row.Children { + cells[i] = extractText(cell) + } + t.SetHeader(cells) + } + } + } +} + +func (r Renderer) tableBody(t *tablewriter.Table, node *ast.TableBody) { + if node := node.AsContainer(); node != nil { + for _, row := range node.Children { + if row := row.AsContainer(); row != nil { + cells := make([]string, len(row.Children)) + for i, cell := range row.Children { + cells[i] = extractText(cell) + } + t.Append(cells) + } + } + } +} + +func (r Renderer) table(w io.Writer, node *ast.Table, entering bool) { + if entering { + w.Write(preformattedToggle) + // gomarkdown appears to only parse headings consisting of a + // single line and always have a TableBody preceded by a single + // TableHeader but we're better off not relying on it + t := tablewriter.NewWriter(w) + t.SetAutoFormatHeaders(false) // TODO: tablewriter options should probably be configurable + if node := node.AsContainer(); node != nil { + for _, child := range node.Children { + switch child := child.(type) { + case *ast.TableHeader: + r.tableHead(t, child) + case *ast.TableBody: + r.tableBody(t, child) + } + } + } + t.Render() + } else { + w.Write(preformattedToggle) + } +} + // RenderNode implements Renderer.RenderNode(). func (r Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus { // despite most of the subroutines here accepting entering, most of @@ -279,6 +351,9 @@ func (r Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Walk r.list(w, node, 0) noNewLine = false } + case *ast.Table: + r.table(w, node, entering) + noNewLine = false } if !noNewLine && !entering { w.Write(lineBreak)