position.go 1.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. package parse
  2. import (
  3. "fmt"
  4. "io"
  5. "strings"
  6. "github.com/tdewolff/parse/buffer"
  7. )
  8. // Position returns the line and column number for a certain position in a file. It is useful for recovering the position in a file that caused an error.
  9. // It only treates \n, \r, and \r\n as newlines, which might be different from some languages also recognizing \f, \u2028, and \u2029 to be newlines.
  10. func Position(r io.Reader, offset int) (line, col int, context string, err error) {
  11. l := buffer.NewLexer(r)
  12. line = 1
  13. for {
  14. c := l.Peek(0)
  15. if c == 0 {
  16. col = l.Pos() + 1
  17. context = positionContext(l, line, col)
  18. err = l.Err()
  19. if err == nil {
  20. err = io.EOF
  21. }
  22. return
  23. }
  24. if offset == l.Pos() {
  25. col = l.Pos() + 1
  26. context = positionContext(l, line, col)
  27. return
  28. }
  29. if c == '\n' {
  30. l.Move(1)
  31. line++
  32. offset -= l.Pos()
  33. l.Skip()
  34. } else if c == '\r' {
  35. if l.Peek(1) == '\n' {
  36. if offset == l.Pos()+1 {
  37. l.Move(1)
  38. continue
  39. }
  40. l.Move(2)
  41. } else {
  42. l.Move(1)
  43. }
  44. line++
  45. offset -= l.Pos()
  46. l.Skip()
  47. } else {
  48. l.Move(1)
  49. }
  50. }
  51. }
  52. func positionContext(l *buffer.Lexer, line, col int) (context string) {
  53. for {
  54. c := l.Peek(0)
  55. if c == 0 && l.Err() != nil || c == '\n' || c == '\r' {
  56. break
  57. }
  58. l.Move(1)
  59. }
  60. // replace unprintable characters by a space
  61. b := l.Lexeme()
  62. for i, c := range b {
  63. if c < 0x20 || c == 0x7F {
  64. b[i] = ' '
  65. }
  66. }
  67. context += fmt.Sprintf("%5d: %s\n", line, string(b))
  68. context += fmt.Sprintf("%s^", strings.Repeat(" ", col+6))
  69. return
  70. }