-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinsert.go
More file actions
141 lines (128 loc) · 3.49 KB
/
Copy pathinsert.go
File metadata and controls
141 lines (128 loc) · 3.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package editpro
import (
"bytes"
"errors"
"fmt"
)
// InsertPosition identifies a file boundary or a line-relative insertion point.
type InsertPosition uint8
const (
InsertStart InsertPosition = iota
InsertEnd
InsertBeforeLine
InsertAfterLine
)
var (
ErrInvalidPosition = errors.New("invalid insert position")
ErrInvalidLine = errors.New("line number must be greater than zero")
ErrLineNotFound = errors.New("line does not exist")
ErrInvalidEOL = errors.New("line ending must be LF or CRLF")
)
// InsertOptions controls line-oriented insertion. Line numbers are 1-based.
// Raw inserts Content exactly. Otherwise Content is separated from adjacent
// lines using EOL, or the source file's first detected line ending when EOL is nil.
type InsertOptions struct {
Position InsertPosition
Line int
Raw bool
EOL []byte
}
// InsertResult describes an insertion without performing file I/O.
type InsertResult struct {
Data []byte
Offset int
Changed bool
}
// Insert inserts content at a file boundary or relative to a source line.
func Insert(src, content []byte, opts InsertOptions) (InsertResult, error) {
if !opts.Raw && len(opts.EOL) > 0 && !bytes.Equal(opts.EOL, []byte("\n")) && !bytes.Equal(opts.EOL, []byte("\r\n")) {
return InsertResult{}, ErrInvalidEOL
}
offset, targetEndedWithEOL, err := insertionOffset(src, opts)
if err != nil {
return InsertResult{}, err
}
if len(content) == 0 {
return InsertResult{Data: src, Offset: offset}, nil
}
prefix, suffix := []byte(nil), []byte(nil)
if !opts.Raw {
eol := opts.EOL
if len(eol) == 0 {
eol = detectEOL(src)
}
switch opts.Position {
case InsertStart, InsertBeforeLine:
if offset < len(src) && !endsWithEOL(content) {
suffix = eol
}
case InsertEnd:
if len(src) > 0 && !endsWithEOL(src) {
prefix = eol
}
case InsertAfterLine:
if offset < len(src) {
if !endsWithEOL(content) {
suffix = eol
}
} else if len(src) > 0 && !targetEndedWithEOL {
prefix = eol
}
}
}
data := make([]byte, 0, len(src)+len(prefix)+len(content)+len(suffix))
data = append(data, src[:offset]...)
data = append(data, prefix...)
data = append(data, content...)
data = append(data, suffix...)
data = append(data, src[offset:]...)
return InsertResult{Data: data, Offset: offset, Changed: true}, nil
}
func insertionOffset(src []byte, opts InsertOptions) (offset int, targetEndedWithEOL bool, err error) {
switch opts.Position {
case InsertStart:
return 0, false, nil
case InsertEnd:
return len(src), endsWithEOL(src), nil
case InsertBeforeLine, InsertAfterLine:
if opts.Line < 1 {
return 0, false, ErrInvalidLine
}
start, ok := lineStart(src, opts.Line)
if !ok {
return 0, false, fmt.Errorf("%w: %d", ErrLineNotFound, opts.Line)
}
if opts.Position == InsertBeforeLine {
return start, false, nil
}
if i := bytes.IndexByte(src[start:], '\n'); i >= 0 {
return start + i + 1, true, nil
}
return len(src), false, nil
default:
return 0, false, ErrInvalidPosition
}
}
func lineStart(src []byte, line int) (int, bool) {
if line == 1 {
return 0, true
}
offset := 0
for current := 1; current < line; current++ {
i := bytes.IndexByte(src[offset:], '\n')
if i < 0 {
return 0, false
}
offset += i + 1
}
return offset, true
}
func detectEOL(src []byte) []byte {
if i := bytes.IndexByte(src, '\n'); i > 0 && src[i-1] == '\r' {
return []byte("\r\n")
}
return []byte("\n")
}
func endsWithEOL(data []byte) bool {
return len(data) > 0 && data[len(data)-1] == '\n'
}