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) }