github.com/jmigpin/editor@v1.6.0/util/parseutil/util.go (about) 1 package parseutil 2 3 import ( 4 "fmt" 5 "net/url" 6 "path/filepath" 7 "runtime" 8 "strings" 9 "unicode" 10 "unicode/utf8" 11 12 "github.com/jmigpin/editor/util/iout/iorw" 13 "github.com/jmigpin/editor/util/mathutil" 14 "github.com/jmigpin/editor/util/osutil" 15 ) 16 17 //---------- 18 19 // TODO: review 20 21 var ExtraRunes = "_-~.%@&?!=#+:^" + "(){}[]<>" + "\\/" + " " 22 23 var excludeResourceRunes = "" + 24 " " + // word separator 25 "=" + // usually around filenames (ex: -arg=/a/b.txt) 26 "(){}[]<>" // usually used around filenames in various outputs 27 // escaped when outputing filenames 28 var escapedInFilenames = excludeResourceRunes + 29 ":" // note: in windows will give "C^:/" 30 31 //---------- 32 33 func AddEscapes(str string, escape rune, escapeRunes string) string { 34 w := []rune{} 35 er := []rune(escapeRunes) 36 for _, ru := range str { 37 if ContainsRune(er, ru) { 38 w = append(w, escape) 39 } 40 w = append(w, ru) 41 } 42 return string(w) 43 } 44 45 func RemoveEscapes(str string, escape rune) string { 46 return RemoveEscapesEscapable(str, escape, "") 47 } 48 func RemoveEscapesEscapable(str string, escape rune, escapable string) string { 49 return string(RemoveEscapes2([]rune(str), []rune(escapable), escape)) 50 } 51 func RemoveEscapes2(rs []rune, escapable []rune, escape rune) []rune { 52 res := make([]rune, 0, len(rs)) 53 escaping := false 54 for i := 0; i < len(rs); i++ { 55 ru := rs[i] 56 if !escaping { 57 if ru == escape { // remove escapes 58 escaping = true 59 continue 60 } 61 } else { 62 escaping = false 63 64 // re-add escape if not one of the escapable 65 if len(escapable) > 0 { 66 if !ContainsRune(escapable, ru) { 67 res = append(res, escape) 68 } 69 } 70 } 71 res = append(res, ru) 72 } 73 return res 74 } 75 76 //---------- 77 78 func EscapeFilename(str string) string { 79 escape := osutil.EscapeRune 80 mustBeEscaped := escapedInFilenames + string(escape) 81 return AddEscapes(str, escape, mustBeEscaped) 82 } 83 84 func RemoveFilenameEscapes(f string, escape, pathSep rune) string { 85 f = RemoveEscapes(f, escape) 86 f = CleanMultiplePathSeps(f, pathSep) 87 if u, err := url.PathUnescape(f); err == nil { 88 f = u 89 } 90 return f 91 } 92 93 //---------- 94 95 func CleanMultiplePathSeps(str string, sep rune) string { 96 w := []rune{} 97 added := false 98 for _, ru := range str { 99 if ru == sep { 100 if !added { 101 added = true 102 w = append(w, ru) 103 } 104 } else { 105 added = false 106 w = append(w, ru) 107 } 108 } 109 return string(w) 110 } 111 112 //---------- 113 114 func ExpandIndexesEscape(rd iorw.ReaderAt, index int, truth bool, fn func(rune) bool, escape rune) (int, int) { 115 // ensure the index is not in the middle of an escape 116 index = ImproveExpandIndexEscape(rd, index, escape) 117 118 l := ExpandLastIndexEscape(rd, index, false, fn, escape) 119 r := ExpandIndexEscape(rd, index, false, fn, escape) 120 return l, r 121 } 122 123 func ExpandIndexEscape(r iorw.ReaderAt, i int, truth bool, fn func(rune) bool, escape rune) int { 124 sc := NewScannerR(r, i) 125 return expandEscape(sc, truth, fn, escape) 126 } 127 128 func ExpandLastIndexEscape(r iorw.ReaderAt, i int, truth bool, fn func(rune) bool, escape rune) int { 129 sc := NewScannerR(r, i) 130 131 // read direction 132 tmp := sc.Reverse 133 sc.Reverse = true 134 defer func() { sc.Reverse = tmp }() // restore 135 136 return expandEscape(sc, truth, fn, escape) 137 } 138 139 func expandEscape(sc *ScannerR, truth bool, fn func(rune) bool, escape rune) int { 140 for { 141 if sc.M.Eof() { 142 break 143 } 144 if err := sc.M.EscapeAny(escape); err == nil { 145 continue 146 } 147 pos0 := sc.KeepPos() 148 ru, err := sc.ReadRune() 149 if err != nil { 150 break 151 } 152 if fn(ru) == truth { 153 pos0.Restore() 154 break 155 } 156 } 157 return sc.Pos() 158 } 159 160 //---------- 161 162 func ImproveExpandIndexEscape(r iorw.ReaderAt, i int, escape rune) int { 163 sc := NewScannerR(r, i) 164 165 // read direction 166 tmp := sc.Reverse 167 sc.Reverse = true 168 defer func() { sc.Reverse = tmp }() // restore 169 170 for { 171 if sc.M.Eof() { 172 break 173 } 174 if err := sc.M.Rune(escape); err == nil { 175 continue 176 } 177 break 178 } 179 return sc.Pos() 180 } 181 182 //---------- 183 184 // Line/col args are one-based. 185 func LineColumnIndex(rd iorw.ReaderAt, line, column int) (int, error) { 186 // must have a good line 187 if line <= 0 { 188 return 0, fmt.Errorf("bad line: %v", line) 189 } 190 line-- // make line 0 the first line 191 192 // tolerate bad columns 193 if column <= 0 { 194 column = 1 195 } 196 column-- // make column 0 the first column 197 198 index := -1 199 l, lStart := 0, 0 200 ri := 0 201 for { 202 if l == line { 203 index = lStart // keep line start in case it is a bad col 204 205 c := ri - lStart 206 if c >= column { 207 index = ri // keep line/col 208 break 209 } 210 } else if l > line { 211 break 212 } 213 214 ru, size, err := iorw.ReadRuneAt(rd, ri) 215 if err != nil { 216 // be tolerant about the column 217 if index >= 0 { 218 return index, nil 219 } 220 return 0, err 221 } 222 ri += size 223 if ru == '\n' { 224 l++ 225 lStart = ri 226 } 227 } 228 if index < 0 { 229 return 0, fmt.Errorf("line not found: %v", line) 230 } 231 return index, nil 232 } 233 234 // Returned line/col values are one-based. 235 func IndexLineColumn(rd iorw.ReaderAt, index int) (int, int, error) { 236 line, lineStart := 0, 0 237 ri := 0 238 for ri < index { 239 ru, size, err := iorw.ReadRuneAt(rd, ri) 240 if err != nil { 241 return 0, 0, err 242 } 243 ri += size 244 if ru == '\n' { 245 line++ 246 lineStart = ri 247 } 248 } 249 line++ // first line is 1 250 col := ri - lineStart + 1 // first column is 1 251 return line, col, nil 252 } 253 254 // Returned line/col values are one-based. 255 func IndexLineColumn2(b []byte, index int) (int, int) { 256 line, lineStart := 0, 0 257 ri := 0 258 for ri < index { 259 ru, size := utf8.DecodeRune(b[ri:]) 260 if size == 0 { 261 break 262 } 263 ri += size 264 if ru == '\n' { 265 line++ 266 lineStart = ri 267 } 268 } 269 line++ // first line is 1 270 col := ri - lineStart + 1 // first column is 1 271 return line, col 272 } 273 274 //---------- 275 276 func DetectEnvVar(str, name string) bool { 277 vstr := "$" + name 278 i := strings.Index(str, vstr) 279 if i < 0 { 280 return false 281 } 282 283 e := i + len(vstr) 284 if e > len(str) { 285 return false 286 } 287 288 // validate rune after the name 289 ru, _ := utf8.DecodeRuneInString(str[e:]) 290 if ru != utf8.RuneError { 291 if unicode.IsLetter(ru) || unicode.IsDigit(ru) || ru == '_' { 292 return false 293 } 294 } 295 296 return true 297 } 298 299 //---------- 300 301 func RunesExcept(runes, except string) string { 302 drop := func(ru rune) rune { 303 if strings.ContainsRune(except, ru) { 304 return -1 305 } 306 return ru 307 } 308 return strings.Map(drop, runes) 309 } 310 311 //---------- 312 313 // Useful to compare src code lines. 314 func TrimLineSpaces(str string) string { 315 return TrimLineSpaces2(str, "") 316 } 317 318 func TrimLineSpaces2(str string, pre string) string { 319 a := strings.Split(str, "\n") 320 u := []string{} 321 for _, s := range a { 322 s = strings.TrimSpace(s) 323 if s != "" { 324 u = append(u, s) 325 } 326 } 327 return pre + strings.Join(u, "\n"+pre) 328 } 329 330 //---------- 331 332 func UrlToAbsFilename(url2 string) (string, error) { 333 u, err := url.Parse(string(url2)) 334 if err != nil { 335 return "", err 336 } 337 if u.Scheme != "file" { 338 return "", fmt.Errorf("expecting file scheme: %v", u.Scheme) 339 } 340 filename := u.Path // unescaped 341 342 if runtime.GOOS == "windows" { 343 // remove leading slash in windows returned by url.parse: https://github.com/golang/go/issues/6027 344 if len(filename) > 0 && filename[0] == '/' { 345 filename = filename[1:] 346 } 347 348 filename = filepath.FromSlash(filename) 349 } 350 351 if !filepath.IsAbs(filename) { 352 return "", fmt.Errorf("filename not absolute: %v", filename) 353 } 354 return filename, nil 355 } 356 357 func AbsFilenameToUrl(filename string) (string, error) { 358 if !filepath.IsAbs(filename) { 359 return "", fmt.Errorf("filename not absolute: %v", filename) 360 } 361 362 if runtime.GOOS == "windows" { 363 filename = filepath.ToSlash(filename) 364 // add leading slash to match UrlToAbsFilename behaviour 365 if len(filename) > 0 && filename[0] != '/' { 366 filename = "/" + filename 367 } 368 } 369 370 u := &url.URL{Scheme: "file", Path: filename} 371 return u.String(), nil // path is escaped 372 } 373 374 //---------- 375 376 func SurroundingString(b []byte, k int, pad int) string { 377 // pad n in each direction for error string 378 i := mathutil.Max(k-pad, 0) 379 i2 := mathutil.Min(k+pad, len(b)) 380 381 if i > i2 { 382 return "" 383 } 384 385 s := string(b[i:i2]) 386 if s == "" { 387 return "" 388 } 389 390 // position indicator (valid after test of empty string) 391 c := k - i 392 393 sep := "●" // "←" 394 s2 := s[:c] + sep + s[c:] 395 if i > 0 { 396 s2 = "..." + s2 397 } 398 if i2 < len(b)-1 { 399 s2 = s2 + "..." 400 } 401 return s2 402 } 403 404 //---------- 405 406 // unquote string with backslash as escape 407 func UnquoteStringBs(s string) (string, error) { 408 return UnquoteString(s, '\\') 409 } 410 411 // removes escapes runes (keeping the escaped) if quoted 412 func UnquoteString(s string, esc rune) (string, error) { 413 rs := []rune(s) 414 _, err := RunesQuote(rs) 415 if err != nil { 416 return "", err 417 } 418 u := RemoveEscapes2(rs[1:len(rs)-1], nil, esc) 419 return string(u), nil 420 } 421 func RunesQuote(rs []rune) (rune, error) { 422 if len(rs) < 2 { 423 return 0, fmt.Errorf("len<2") 424 } 425 quotes := []rune("\"'`") // allowed quotes 426 quote := rs[0] 427 if !ContainsRune(quotes, quote) { 428 return 0, fmt.Errorf("unexpected starting quote: %q", quote) 429 } 430 if rs[len(rs)-1] != quote { 431 return 0, fmt.Errorf("missing ending quote: %q", quote) 432 } 433 return quote, nil 434 } 435 func IsQuoted(s string) bool { 436 _, err := RunesQuote([]rune(s)) 437 return err == nil 438 } 439 440 //---------- 441 442 func ContainsRune(rs []rune, ru rune) bool { 443 for _, ru2 := range rs { 444 if ru2 == ru { 445 return true 446 } 447 } 448 return false 449 }