1package parse
2
3import (
4 "fmt"
5 "io"
6 "strings"
7 "unicode"
8)
9
10// 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.
11// 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.
12func Position(r io.Reader, offset int) (line, col int, context string) {
13 l := NewInput(r)
14 line = 1
15 for l.Pos() < offset {
16 c := l.Peek(0)
17 n := 1
18 newline := false
19 if c == '\n' {
20 newline = true
21 } else if c == '\r' {
22 if l.Peek(1) == '\n' {
23 newline = true
24 n = 2
25 } else {
26 newline = true
27 }
28 } else if c >= 0xC0 {
29 var r rune
30 if r, n = l.PeekRune(0); r == '\u2028' || r == '\u2029' {
31 newline = true
32 }
33 } else if c == 0 && l.Err() != nil {
34 break
35 }
36
37 if 1 < n && offset < l.Pos()+n {
38 break
39 }
40 l.Move(n)
41
42 if newline {
43 line++
44 offset -= l.Pos()
45 l.Skip()
46 }
47 }
48
49 col = len([]rune(string(l.Lexeme()))) + 1
50 context = positionContext(l, line, col)
51 return
52}
53
54func positionContext(l *Input, line, col int) (context string) {
55 for {
56 c := l.Peek(0)
57 if c == 0 && l.Err() != nil || c == '\n' || c == '\r' {
58 break
59 }
60 l.Move(1)
61 }
62 rs := []rune(string(l.Lexeme()))
63
64 // cut off front or rear of context to stay between 60 characters
65 limit := 60
66 offset := 20
67 ellipsisFront := ""
68 ellipsisRear := ""
69 if limit < len(rs) {
70 if col <= limit-offset {
71 ellipsisRear = "..."
72 rs = rs[:limit-3]
73 } else if col >= len(rs)-offset-3 {
74 ellipsisFront = "..."
75 col -= len(rs) - offset - offset - 7
76 rs = rs[len(rs)-offset-offset-4:]
77 } else {
78 ellipsisFront = "..."
79 ellipsisRear = "..."
80 rs = rs[col-offset-1 : col+offset]
81 col = offset + 4
82 }
83 }
84
85 // replace unprintable characters by a space
86 for i, r := range rs {
87 if !unicode.IsGraphic(r) {
88 rs[i] = '·'
89 }
90 }
91
92 context += fmt.Sprintf("%5d: %s%s%s\n", line, ellipsisFront, string(rs), ellipsisRear)
93 context += fmt.Sprintf("%s^", strings.Repeat(" ", 6+col))
94 return
95}