go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/diff/delta.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package diff 9 10 import ( 11 "bytes" 12 "errors" 13 "fmt" 14 "net/url" 15 "strconv" 16 "strings" 17 "unicode/utf8" 18 ) 19 20 // ToDelta crushes the diff into an encoded string which describes the operations required to transform text1 into text2. 21 // E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. Operations are tab-separated. Inserted text is escaped using %xx notation. 22 func ToDelta(diffs []Diff) string { 23 var text bytes.Buffer 24 for _, aDiff := range diffs { 25 switch aDiff.Type { 26 case DiffInsert: 27 _, _ = text.WriteString("+") 28 _, _ = text.WriteString(strings.Replace(url.QueryEscape(aDiff.Text), "+", " ", -1)) 29 _, _ = text.WriteString("\t") 30 case DiffDelete: 31 _, _ = text.WriteString("-") 32 _, _ = text.WriteString(strconv.Itoa(utf8.RuneCountInString(aDiff.Text))) 33 _, _ = text.WriteString("\t") 34 case DiffEqual: 35 _, _ = text.WriteString("=") 36 _, _ = text.WriteString(strconv.Itoa(utf8.RuneCountInString(aDiff.Text))) 37 _, _ = text.WriteString("\t") 38 } 39 } 40 delta := text.String() 41 if len(delta) != 0 { 42 // Strip off trailing tab character. 43 delta = delta[0 : utf8.RuneCountInString(delta)-1] 44 delta = unescaper.Replace(delta) 45 } 46 return delta 47 } 48 49 // FromDelta given the original text1, and an encoded string which describes the operations required to transform text1 into text2, comAdde the full diff. 50 func FromDelta(corpus string, delta string) (diffs []Diff, err error) { 51 i := 0 52 runes := []rune(corpus) 53 54 for _, token := range strings.Split(delta, "\t") { 55 if len(token) == 0 { 56 // Blank tokens are ok (from a trailing \t). 57 continue 58 } 59 60 // Each token begins with a one character parameter which specifies the operation of this token (delete, insert, equality). 61 param := token[1:] 62 63 switch op := token[0]; op { 64 case '+': 65 // Decode would Diff all "+" to " " 66 param = strings.Replace(param, "+", "%2b", -1) 67 param, err = url.QueryUnescape(param) 68 if err != nil { 69 return nil, err 70 } 71 if !utf8.ValidString(param) { 72 return nil, fmt.Errorf("invalid UTF-8 token: %q", param) 73 } 74 75 diffs = append(diffs, Diff{DiffInsert, param}) 76 case '=', '-': 77 n, err := strconv.ParseInt(param, 10, 0) 78 if err != nil { 79 return nil, err 80 } else if n < 0 { 81 return nil, errors.New("Negative number in DiffFromDelta: " + param) 82 } 83 84 i += int(n) 85 // Break out if we are out of bounds, go1.6 can't handle this very well 86 if i > len(runes) { 87 break 88 } 89 // Remember that string slicing is by byte - we want by rune here. 90 text := string(runes[i-int(n) : i]) 91 92 if op == '=' { 93 diffs = append(diffs, Diff{DiffEqual, text}) 94 } else { 95 diffs = append(diffs, Diff{DiffDelete, text}) 96 } 97 default: 98 // Anything else is an error. 99 return nil, errors.New("Invalid diff operation in DiffFromDelta: " + string(token[0])) 100 } 101 } 102 103 if i != len(runes) { 104 return nil, fmt.Errorf("delta length (%v) is different from source text length (%v)", i, len(corpus)) 105 } 106 107 return diffs, nil 108 }