diff --git a/go.sum b/go.sum index 83aa893..115f78d 100644 --- a/go.sum +++ b/go.sum @@ -7,13 +7,11 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/gonvenience/term v1.0.0 h1:joCB/j0Ngmdakd3muuLgAGPMf7DNKdoe708c1I6RiBs= github.com/gonvenience/term v1.0.0/go.mod h1:wohD4Iqso9Eol7qc2VnNhSFFhZxok5PvO7pZhdrAn4E= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= @@ -29,25 +27,19 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= -github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= -github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -60,12 +52,10 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -77,9 +67,7 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/parse.go b/parse.go index 6d5c812..82f3cca 100644 --- a/parse.go +++ b/parse.go @@ -21,8 +21,10 @@ package bunt import ( + "bufio" "bytes" "fmt" + "io" "regexp" "sort" "strconv" @@ -48,6 +50,189 @@ func ProcessTextAnnotations() ParseOption { } } +// ParseStream reads from the input reader and parses all supported ANSI +// sequences that are relevant for colored strings. +// +// Please notes: This function is still under development and should replace +// the `ParseString` function code eventually. +func ParseStream(in io.Reader, opts ...ParseOption) (*String, error) { + var input *bufio.Reader + switch typed := in.(type) { + case *bufio.Reader: + input = typed + + default: + input = bufio.NewReader(in) + } + + type seq struct { + values string + suffix rune + } + + var readSGR = func() seq { + var buf bytes.Buffer + for { + r, _, err := input.ReadRune() + if err == io.EOF { + break + } + + switch r { + case 'h', 'l', 'm', 'r', 'A', 'B', 'C', 'D', 'H', 'f', 'g', 'K', 'J', 'y', 'q': + return seq{ + values: buf.String(), + suffix: r, + } + + default: + buf.WriteRune(r) + } + } + + panic("failed to parse ANSI sequence") + } + + var skipUntil = func(end rune) { + for { + r, _, err := input.ReadRune() + if err == io.EOF { + panic("reached end of file before reaching end identifier") + } + + if r == end { + return + } + } + } + + var result String + var line String + var lineIdx uint + var lineCap uint + var settings uint64 + + var readANSISeq = func() { + r, _, err := input.ReadRune() + if err == io.EOF { + return + } + + // https://en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences + switch r { + case '[': + var seq = readSGR() + switch seq.suffix { + case 'm': // colors + settings, err = parseSelectGraphicRenditionEscapeSequence(seq.values) + if err != nil { + panic(err) + } + + case 'K': // clear line + var start, end uint + switch seq.values { + case "", "0": + start, end = lineIdx, lineCap + + case "1": + start, end = 0, lineIdx + + case "2": + start, end = 0, lineCap + } + + for i := start; i < end; i++ { + line[i].Symbol = ' ' + line[i].Settings = 0 + } + + default: + // ignoring all other sequences + } + + case ']': + skipUntil('\a') + } + } + + var add = func(r rune) { + var cr = ColoredRune{Symbol: r, Settings: settings} + + if lineIdx < lineCap { + line[lineIdx] = cr + lineIdx++ + + } else { + line = append(line, cr) + lineIdx++ + lineCap++ + } + } + + var del = func() { + line = line[:len(line)-1] + lineIdx-- + lineCap-- + } + + var flush = func() { + // Prepare to remove trailing spaces by finding the last non-space rune + var endIdx = len(line) - 1 + for ; endIdx >= 0; endIdx-- { + if line[endIdx].Symbol != ' ' { + break + } + } + + result = append(result, line[:endIdx+1]...) + + line = String{} + lineIdx = 0 + lineCap = 0 + } + + var newline = func() { + flush() + result = append(result, ColoredRune{Symbol: '\n'}) + } + + for { + r, _, err := input.ReadRune() + if err != nil && err == io.EOF { + break + } + + switch r { + case '\x1b': + readANSISeq() + + case '\r': + lineIdx = 0 + + case '\n': + newline() + + case '\b': + del() + + default: + add(r) + } + } + + flush() + + // Process optional parser options + for _, opt := range opts { + if err := opt(&result); err != nil { + return nil, err + } + } + + return &result, nil +} + // ParseString parses a string that can contain both ANSI escape code Select // Graphic Rendition (SGR) codes and Markdown style text annotations, for // example *bold* or _italic_. diff --git a/parse_test.go b/parse_test.go index 0319864..3b5fc3d 100644 --- a/parse_test.go +++ b/parse_test.go @@ -21,6 +21,8 @@ package bunt_test import ( + "strings" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -38,6 +40,16 @@ var _ = Describe("parse input string", func() { TrueColorSetting = AUTO }) + Context("parse supported ANSI sequences from an input reader", func() { + It("should parse an example command line input without errors", func() { + example := "Hallo\rHello, \x1b[1mWorld\x1b[0m!" + + result, err := ParseStream(strings.NewReader(example)) + Expect(err).ToNot(HaveOccurred()) + Expect(result.String()).To(Equal("Hello, \x1b[1mWorld\x1b[0m!")) + }) + }) + Context("parse Select Graphic Rendition (SGR) based input", func() { It("should parse an input string with SGR parameters", func() { input := "Example: \x1b[1mbold\x1b[0m, \x1b[3mitalic\x1b[0m, \x1b[4munderline\x1b[0m, \x1b[38;2;133;247;7mforeground\x1b[0m, and \x1b[48;2;133;247;7mbackground\x1b[0m."