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  }