2020-08-06 20:58:47 +03:00
|
|
|
// Package markdownw implements writers for Markdown.
|
|
|
|
package markdownw
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2024-01-15 04:45:34 +03:00
|
|
|
"context"
|
2020-08-06 20:58:47 +03:00
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"html"
|
|
|
|
"io"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2023-05-11 05:03:45 +03:00
|
|
|
"sync"
|
2020-08-06 20:58:47 +03:00
|
|
|
"time"
|
|
|
|
|
2023-11-22 00:49:52 +03:00
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
|
2023-05-07 05:36:34 +03:00
|
|
|
"github.com/neilotoole/sq/cli/output"
|
2020-08-23 13:42:15 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/kind"
|
2023-11-20 04:06:36 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/record"
|
2020-08-23 13:42:15 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/stringz"
|
2020-08-06 20:58:47 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// RecordWriter implements output.RecordWriter.
|
|
|
|
type RecordWriter struct {
|
|
|
|
out io.Writer
|
2024-01-27 10:11:24 +03:00
|
|
|
pr *output.Printing
|
2020-08-06 20:58:47 +03:00
|
|
|
buf *bytes.Buffer
|
2024-01-27 10:11:24 +03:00
|
|
|
recMeta record.Meta
|
|
|
|
mu sync.Mutex
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
|
2023-05-22 18:08:14 +03:00
|
|
|
var _ output.NewRecordWriterFunc = NewRecordWriter
|
|
|
|
|
2020-08-06 20:58:47 +03:00
|
|
|
// NewRecordWriter returns a writer instance.
|
2023-05-22 18:08:14 +03:00
|
|
|
func NewRecordWriter(out io.Writer, pr *output.Printing) output.RecordWriter {
|
2023-05-07 05:36:34 +03:00
|
|
|
return &RecordWriter{out: out, pr: pr}
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Open implements output.RecordWriter.
|
2024-01-15 04:45:34 +03:00
|
|
|
func (w *RecordWriter) Open(_ context.Context, recMeta record.Meta) error {
|
2020-08-06 20:58:47 +03:00
|
|
|
w.recMeta = recMeta
|
|
|
|
w.buf = &bytes.Buffer{}
|
|
|
|
|
|
|
|
// Write the header
|
|
|
|
for i, field := range recMeta {
|
|
|
|
w.buf.WriteString("| ")
|
2023-07-09 04:34:53 +03:00
|
|
|
w.buf.WriteString(field.MungedName() + " ")
|
2020-08-06 20:58:47 +03:00
|
|
|
|
|
|
|
if i == len(recMeta)-1 {
|
|
|
|
w.buf.WriteString("|\n")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the header separator row
|
|
|
|
for i := range recMeta {
|
|
|
|
w.buf.WriteString("| --- ")
|
|
|
|
|
|
|
|
if i == len(recMeta)-1 {
|
|
|
|
w.buf.WriteString("|\n")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Flush implements output.RecordWriter.
|
2024-01-15 04:45:34 +03:00
|
|
|
func (w *RecordWriter) Flush(context.Context) error {
|
2020-08-06 20:58:47 +03:00
|
|
|
_, err := w.buf.WriteTo(w.out) // resets buf
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close implements output.RecordWriter.
|
2024-01-15 04:45:34 +03:00
|
|
|
func (w *RecordWriter) Close(ctx context.Context) error {
|
|
|
|
return w.Flush(ctx)
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
|
2023-05-22 18:08:14 +03:00
|
|
|
func (w *RecordWriter) writeRecord(rec record.Record) error {
|
2020-08-06 20:58:47 +03:00
|
|
|
var s string
|
|
|
|
for i, field := range rec {
|
|
|
|
w.buf.WriteString("| ")
|
|
|
|
|
|
|
|
switch val := field.(type) {
|
|
|
|
default:
|
|
|
|
// should never happen
|
|
|
|
s = escapeMarkdown(fmt.Sprintf("%v", val))
|
|
|
|
|
|
|
|
case nil:
|
|
|
|
// nil is rendered as empty string, which this cell already is
|
2023-05-27 16:57:07 +03:00
|
|
|
case int64:
|
|
|
|
s = strconv.FormatInt(val, 10)
|
|
|
|
case string:
|
|
|
|
s = escapeMarkdown(val)
|
2023-11-22 00:49:52 +03:00
|
|
|
case decimal.Decimal:
|
|
|
|
s = stringz.FormatDecimal(val)
|
2023-05-27 16:57:07 +03:00
|
|
|
case bool:
|
|
|
|
s = strconv.FormatBool(val)
|
|
|
|
case float64:
|
|
|
|
s = stringz.FormatFloat(val)
|
|
|
|
case []byte:
|
|
|
|
s = base64.StdEncoding.EncodeToString(val)
|
|
|
|
case time.Time:
|
2022-12-18 08:16:10 +03:00
|
|
|
switch w.recMeta[i].Kind() { //nolint:exhaustive
|
2020-08-06 20:58:47 +03:00
|
|
|
default:
|
2023-05-27 16:57:07 +03:00
|
|
|
s = w.pr.FormatDatetime(val)
|
2020-08-23 13:42:15 +03:00
|
|
|
case kind.Time:
|
2023-05-27 16:57:07 +03:00
|
|
|
s = w.pr.FormatTime(val)
|
2020-08-23 13:42:15 +03:00
|
|
|
case kind.Date:
|
2023-05-27 16:57:07 +03:00
|
|
|
s = w.pr.FormatDate(val)
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
w.buf.WriteString(s + " ")
|
|
|
|
}
|
|
|
|
|
|
|
|
w.buf.WriteString("|\n")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteRecords implements output.RecordWriter.
|
2024-01-15 04:45:34 +03:00
|
|
|
func (w *RecordWriter) WriteRecords(ctx context.Context, recs []record.Record) error {
|
2023-05-11 05:03:45 +03:00
|
|
|
w.mu.Lock()
|
|
|
|
defer w.mu.Unlock()
|
|
|
|
|
2020-08-06 20:58:47 +03:00
|
|
|
var err error
|
|
|
|
for _, rec := range recs {
|
2024-01-15 04:45:34 +03:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
default:
|
|
|
|
}
|
2020-08-06 20:58:47 +03:00
|
|
|
err = w.writeRecord(rec)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// escapeMarkdown is quick effort at escaping markdown
|
|
|
|
// table cell text. It is not at all tested. Replace this
|
|
|
|
// function with a real library call at the earliest opportunity.
|
|
|
|
func escapeMarkdown(s string) string {
|
|
|
|
s = html.EscapeString(s)
|
2022-12-18 09:07:38 +03:00
|
|
|
s = strings.ReplaceAll(s, "|", "|")
|
|
|
|
s = strings.ReplaceAll(s, "\r\n", "<br/>")
|
|
|
|
s = strings.ReplaceAll(s, "\n", "<br/>")
|
2020-08-06 20:58:47 +03:00
|
|
|
return s
|
|
|
|
}
|