You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

197 lines
3.8 KiB
Go

package main
import (
"bufio"
"fmt"
"io"
"strings"
)
const DefaultTabWidth = 8
type Token struct {
Name string
Pos int
Length int
}
type Parser struct {
Width int
}
func (p Parser) IterMap(r io.Reader, onValue func(map[string]string) error) error {
return p.IterSlice(r, func(headers, row []string) error {
return onValue(zip(headers, row))
})
}
func (p Parser) IterSlice(r io.Reader, onValue func([]string, []string) error) error {
var headers []string
return iter{
Reader: r,
Width: p.width(),
OnHeaders: func(h []string) error {
headers = h
return nil
},
OnRow: func(row []string) error {
return onValue(headers, row)
},
}.Run()
}
func (p Parser) Parse(r io.Reader) (Document, error) {
var doc Document
return doc, iter{
Reader: r,
OnHeaders: doc.setHeader,
OnRow: doc.addRow,
Width: p.width(),
}.Run()
}
func (p Parser) width() int {
if p.Width == 0 {
return DefaultTabWidth
}
return p.Width
}
type iter struct {
Reader io.Reader
Width int
OnHeaders func([]string) error
OnRow func([]string) error
}
func (i iter) Run() error {
s := bufio.NewScanner(i.Reader)
var tokens []Token
if s.Scan() {
tokens = i.tokens(s.Text())
headers := make([]string, len(tokens), len(tokens))
for i, header := range tokens {
headers[i] = header.Name
}
if err := i.OnHeaders(headers); err != nil {
return fmt.Errorf("failed to handle headers: %w", err)
}
}
if err := s.Err(); err != nil {
return fmt.Errorf("failed to read header line: %w", err)
}
for s.Scan() {
if err := i.OnRow(i.parse(tokens, s.Text())); err != nil {
return fmt.Errorf("failed to handle values: %w", err)
}
}
if err := s.Err(); err != nil {
return fmt.Errorf("failed to read input: %w", err)
}
return nil
}
func (i iter) tokens(line string) []Token {
index := 0
pos := 0
tokens := make([]Token, 0)
word := make([]rune, 0, len([]rune(line)))
for _, char := range line {
if char != '\t' {
if len(word) == 0 {
index = pos
}
word = append(word, char)
pos += 1
} else {
if len(word) == 0 {
pos += i.Width
if len(tokens) > 0 {
tokens[len(tokens)-1].Length += i.Width
}
} else {
padding := i.Width - (len(word) % i.Width)
pos += padding
tokens = append(tokens, Token{
Name: string(word),
Length: len(word) + padding,
Pos: index,
})
word = word[len(word):]
}
}
}
if len(word) != 0 {
tokens = append(tokens, Token{
Name: string(word),
Length: len(word),
Pos: index,
})
}
return tokens
}
func (i iter) parse(headings []Token, l string) []string {
line := removeTabs(i.Width, l)
values := make([]string, len(headings), len(headings))
var value string
for j, heading := range headings {
if len(line) >= heading.Pos+heading.Length {
if j >= len(headings)-1 {
value = line[heading.Pos:]
} else {
value = line[heading.Pos : heading.Pos+heading.Length]
}
} else if len(line) >= heading.Pos {
value = line[heading.Pos:]
} else {
value = ""
}
values[j] = strings.TrimSpace(value)
}
return values
}
func zip[K comparable, V any](keys []K, values []V) map[K]V {
ret := make(map[K]V, len(keys))
for i, key := range keys {
if i < len(values) {
ret[key] = values[i]
} else {
ret[key] = *new(V)
}
}
return ret
}
func removeTabs(width int, line string) string {
var ret strings.Builder
var word []rune
for _, char := range line {
if char != '\t' {
word = append(word, char)
} else {
if len(word) == 0 {
ret.WriteString(spaces(width))
} else {
ret.WriteString(string(word) + spaces(width-(len(word)%width)))
word = make([]rune, 0)
}
}
}
ret.WriteString(string(word))
return ret.String()
}
func spaces(length int) string {
n := max(length, 0)
ret := make([]rune, n)
for i := 0; i < n; i++ {
ret[i] = ' '
}
return string(ret)
}