1package fileio23import (4 "bufio"5 "context"6 "fmt"7 "io"8 "log/slog"9 "slices"1011 "src.rest/cat/012/internal/telemetry"12)1314// Streams: 012 - reads a table from standard input, and 012 --pipe15// writes one to standard output. A stream has no name to tell its16// format by, so ImportReader looks at the text: NUON (or JSON, which is17// NUON too) starts with [ or {, and anything else is read as delimited18// text, CSV or TSV by the delimiter found.1920// ImportReader reads a table from r into a new sheet named name (as a21// file's sheet is named after the file), telling its format from the22// text; Result.Kind is NUON, JSON, CSV or TSV. It checks ctx between23// rows and updates opt.Progress.24func ImportReader(ctx context.Context, name string, r io.Reader, opt Options) (*Result, error) {25 span := telemetry.ParentFrom(ctx).Start("import", slog.String("format", "stream"))26 res, err := importReader(telemetry.WithParent(ctx, span.Parent()), r, opt)27 if err != nil {28 span.Fail(err)29 return nil, err30 }31 if book := res.Sheet.Book(); book.RenameSheet(res.Sheet, sheetName(name)) == nil {32 book.ClearHistory()33 }34 res.Sheet.LoadFitWidths() // a stream is CSV, TSV, JSON or NUON: no widths of its own35 if p := opt.Progress; p != nil {36 p.permille.Store(1000)37 }38 span.End(slog.String("kind", res.Kind.String()), slog.Int("rows", res.Rows))39 return res, nil40}4142func importReader(ctx context.Context, r io.Reader, opt Options) (*Result, error) {43 defer context.AfterFunc(ctx, func() {44 if c, ok := r.(io.Closer); ok {45 c.Close() // unblocks a read from a pipe46 }47 })()48 br := bufio.NewReaderSize(r, sniffSize)49 progress := func(rows int) { opt.Progress.setRows(rows) }50 switch first, err := firstByte(br); {51 case err != nil:52 return nil, err53 case first == '[' || first == '{':54 res, json, err := readTable(ctx, br, opt.MaxCells, progress)55 if err != nil {56 return nil, err57 }58 res.Kind = NUON59 if json {60 res.Kind = JSON61 }62 return res, nil63 }64 s, rows, notes, comma, err := readDelimitedAs(ctx, br, CSV, opt.MaxCells, opt.Locale, progress)65 if err != nil {66 return nil, err67 }68 res := &Result{Sheet: s, Kind: CSV, Rows: rows, Notes: notes}69 if comma == '\t' {70 res.Kind = TSV71 res.Notes = slices.DeleteFunc(notes, func(n string) bool { return n == "separated by tabs" }) // it's TSV72 }73 return res, nil74}7576// firstByte is the first byte of the text past a byte order mark and77// spaces, without taking it; 0 for text of nothing else.78func firstByte(br *bufio.Reader) (byte, error) {79 for n := 1; n <= sniffSize; n++ {80 b, err := br.Peek(n)81 if len(b) < n {82 if err == io.EOF {83 err = nil84 }85 return 0, err86 }87 switch c := b[n-1]; {88 case c == ' ' || c == '\t' || c == '\n' || c == '\r':89 case n <= 3 && string(b[:n]) == "\xEF\xBB\xBF"[:n]:90 default:91 return c, nil92 }93 }94 return 0, nil95}9697// Encode writes a snapshot to w in the text format k (see Kind.IsText):98// what 012 --pipe writes to standard output.99func Encode(w io.Writer, k Kind, snap *Snapshot) (*ExportResult, error) {100 encode := k.format().encode101 if encode == nil {102 return nil, fmt.Errorf("can't write %s to a stream", k)103 }104 rows, err := encode(w, snap)105 if err != nil {106 return nil, err107 }108 return &ExportResult{Rows: rows, Notes: formulaNotes(snap)}, nil109}