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