-
-
Notifications
You must be signed in to change notification settings - Fork 24
/
hyperlink.go
323 lines (271 loc) · 9.06 KB
/
hyperlink.go
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
// Copyright (c) 2017 Andrey Gayvoronsky <plandem@gmail.com>
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package hyperlink
import (
"fmt"
sharedML "github.com/plandem/ooxml/ml"
"github.com/plandem/xlsx/format/styles"
"github.com/plandem/xlsx/internal"
"github.com/plandem/xlsx/internal/ml"
"github.com/plandem/xlsx/internal/validator"
"github.com/plandem/xlsx/types"
"regexp"
"strings"
)
//Info hold advanced settings of hyperlink
type Info struct {
hyperlink *ml.Hyperlink
format interface{}
linkType hyperlinkType
}
//Option is helper type to set options for hyperlink
type Option func(o *Info)
type hyperlinkType byte
const (
hyperlinkTypeUnknown hyperlinkType = iota
hyperlinkTypeWeb
hyperlinkTypeEmail
hyperlinkTypeFile
)
//New creates and returns a new Info object that holds settings for hyperlink and related styles
func New(options ...Option) *Info {
i := &Info{
hyperlink: &ml.Hyperlink{},
}
i.Set(options...)
return i
}
//Set sets new options for hyperlink
func (i *Info) Set(options ...Option) {
for _, o := range options {
o(i)
}
}
//nolint
//Validate validates hyperlink info and return error in case of invalid settings
func (i *Info) Validate() error {
switch i.linkType {
case hyperlinkTypeUnknown:
if len(i.hyperlink.Location) == 0 {
return fmt.Errorf("unknown type of hyperlink")
}
case hyperlinkTypeWeb:
if len(i.hyperlink.RID) > internal.UrlLimit {
return fmt.Errorf("url exceeded maximum allowed length (%d chars)", internal.UrlLimit)
}
if len(i.hyperlink.RID) <= 3 {
return fmt.Errorf("url is too short")
}
if strings.Contains(string(i.hyperlink.RID), "#") {
return fmt.Errorf("url contains a pound sign (#)")
}
if !validator.IsURL(string(i.hyperlink.RID)) {
return fmt.Errorf("url is not valid")
}
case hyperlinkTypeEmail:
if len(i.hyperlink.RID) > internal.UrlLimit {
return fmt.Errorf("email exceeded maximum allowed length (%d chars)", internal.UrlLimit)
}
if !validator.IsEmail(string(i.hyperlink.RID)) {
if ok, info := validator.IsMailTo(string(i.hyperlink.RID)); ok && validator.IsEmail(info["email"]) {
break
}
return fmt.Errorf("email is not valid")
}
case hyperlinkTypeFile:
if len(i.hyperlink.RID) > internal.UrlLimit {
return fmt.Errorf("link to file exceeded maximum allowed length (%d chars)", internal.UrlLimit)
}
if len(i.hyperlink.RID) <= 3 {
return fmt.Errorf("filename is too short")
}
if strings.Contains(string(i.hyperlink.RID), "#") {
return fmt.Errorf("filename contains a pound sign (#)")
}
}
return nil
}
//String returns text version of hyperlink info
func (i *Info) String() string {
target := string(i.hyperlink.RID)
location := i.hyperlink.Location
if len(location) > 0 {
return fmt.Sprintf("%s#%s", target, location)
}
return target
}
//Styles sets style format to requested DirectStyleID or styles.Info
func Styles(s interface{}) Option {
return func(i *Info) {
i.format = s
}
}
//Tooltip adds a tooltip information for hyperlink
func Tooltip(tip string) Option {
return func(i *Info) {
i.hyperlink.Tooltip = tip
}
}
//Display adds a display information for hyperlink
func Display(display string) Option {
return func(i *Info) {
i.hyperlink.Display = display
}
}
//ToMail sets target to email
func ToMail(address, subject string) Option {
return func(i *Info) {
if len(subject) > 0 {
i.hyperlink.RID = sharedML.RID(fmt.Sprintf("mailto:%s?subject=%s", address, subject))
} else {
i.hyperlink.RID = sharedML.RID(fmt.Sprintf("mailto:%s", address))
}
i.linkType = hyperlinkTypeEmail
}
}
//ToUrl sets target to web site
func ToUrl(address string) Option {
return func(i *Info) {
i.hyperlink.RID = sharedML.RID(escapeTarget(strings.TrimRight(address, `/`)))
i.linkType = hyperlinkTypeWeb
}
}
//ToFile sets target to external file
func ToFile(fileName string) Option {
return func(i *Info) {
//change the directory separator from Unix to DOS
fileName = strings.Replace(fileName, "/", "\\", -1)
//add the file:/// URI to the url for Windows style "C:/" link and network shares
if validator.IsWinPath(fileName) {
fileName = "file:///" + fileName
}
//convert a '.\dir\filename' link to 'dir\filename'
re := regexp.MustCompile(`^\.\\`)
fileName = re.ReplaceAllString(fileName, "")
i.hyperlink.RID = sharedML.RID(escapeTarget(fileName))
i.linkType = hyperlinkTypeFile
}
}
//ToRef sets target to ref of sheet with sheetName. Omit sheetName to set location to ref of active sheet
func ToRef(ref types.Ref, sheetName string) Option {
return func(i *Info) {
if len(ref) > 0 {
if len(sheetName) > 0 {
//sheet + ref
i.hyperlink.Location = fmt.Sprintf("%s!%s", escapeLocation(sheetName), ref)
} else {
//ref only, can be cell or bookmark
i.hyperlink.Location = fmt.Sprintf("%s", ref)
}
}
}
}
//ToBookmark sets target to bookmark, that can be named region in xlsx, bookmark of remote file or even site
func ToBookmark(location string) Option {
return func(i *Info) {
if len(location) > 0 {
if location[0] == '#' {
location = location[1:]
}
//ref only, can be cell or bookmark
i.hyperlink.Location = fmt.Sprintf("%s", escapeLocation(location))
}
}
}
/*
ToTarget is very close to HYPERLINK function of Excel
https://support.office.com/en-us/article/hyperlink-function-333c7ce6-c5ae-4164-9c47-7de9b76f577f
a) to target: "target" or "[target]"
b) to location at target: "[target]location" or "target#location"
Here are some examples of supported values:
- same file, same sheet
=HYPERLINK("#A1", "Reference to same sheet")
- same file, other sheet
=HYPERLINK("#SheetName!A1", "Reference to sheet without space in name")
=HYPERLINK("#'Sheet Name'!A1", "Reference to sheet with space in name")
- other local file
=HYPERLINK("D:\Folder\File.docx","Word file")
=HYPERLINK("D:\Folder\File.docx#Bookmark","Local Word file with bookmark")
=HYPERLINK("D:\Folder\File.xlsx#SheetName!A1","Local Excel file with reference")
=HYPERLINK("D:\Folder\File.xlsx#'Sheet Name'!A1","Local Excel file with reference")
=HYPERLINK("[D:\Folder\File.docx]","Word file")
=HYPERLINK("[D:\Folder\File.docx]Bookmark","Local Word file with bookmark")
=HYPERLINK("[D:\Folder\File.xlsx]SheetName!A1","Local Excel file with reference")
=HYPERLINK("[D:\Folder\File.xlsx]'Sheet Name'!A1","Local Excel file with reference")
- other remote file
=HYPERLINK("\\SERVER\Folder\File.doc", "Remote Word file")
=HYPERLINK("\\SERVER\Folder\File.xlsx#SheetName!A1", "Remote Excel file with reference")
=HYPERLINK("\\SERVER\Folder\File.xlsx#'Sheet Name'!A1", "Remote Excel file with reference")
=HYPERLINK("[\\SERVER\Folder\File.xlsx]SheetName!A1", "Remote Excel file with reference")
=HYPERLINK("[\\SERVER\Folder\File.xlsx]'Sheet Name'!A1", "Remote Excel file with reference")
- url
=HYPERLINK("https://www.spam.it","Website without bookmark")
=HYPERLINK("https://www.spam.it/#bookmark","Website with bookmark")
=HYPERLINK("[https://www.spam.it/]bookmark","Website with bookmark")
-email
=HYPERLINK("mailto:spam@spam.it","Email without subject")
=HYPERLINK("mailto:spam@spam.it?subject=topic","Email with subject")
*/
func ToTarget(target string) Option {
return func(i *Info) {
var location string
//location is set using pound sign (#)
if i := strings.LastIndexByte(target, '#'); i != -1 {
location = target[i+1:]
target = target[:i]
} else if i = strings.LastIndexByte(target, ']'); target[0] == '[' && i != -1 {
location = target[i+1:]
target = target[1:i]
}
if len(location) > 0 {
//TODO: potential corrupted location. Ideally it should be parsed and set via 'ToBookmark' or 'ToRef'
i.hyperlink.Location = location
}
//detect type of link and call related method to set proper info
if len(target) > 0 {
if validator.IsURL(target) {
i.Set(ToUrl(target))
} else if ok, mail := validator.IsMailTo(target); ok {
i.Set(ToMail(mail["email"], mail["subject"]))
} else if validator.IsEmail(target) {
i.Set(ToMail(target, ""))
} else if validator.IsFilePath(target) {
i.Set(ToFile(target))
} else {
panic(fmt.Sprintf("Can't detect type of hyperlink for target: %s", target))
}
}
}
}
//private method used by hyperlinks manager to unpack Info
func from(info *Info) (hyperlink *ml.Hyperlink, format interface{}, err error) {
if err = info.Validate(); err != nil {
return
}
format = info.format
hyperlink = info.hyperlink
return
}
//private method used by hyperlinks manager to pack Info
func to(link *ml.Hyperlink, target string, styleID styles.DirectStyleID) *Info {
//normalize location
location := link.Location
if len(location) > 0 && location[0] != '#' {
location = "#" + location
}
return New(
Styles(styleID),
Display(link.Display),
Tooltip(link.Tooltip),
ToTarget(target+location),
)
}
func escapeLocation(location string) string {
// TODO: escape location (research what kind of escaping Excel is expecting)
return `'` + strings.Replace(location, `'`, `\'`, -1) + `'`
}
func escapeTarget(target string) string {
//pound symbol (#) is not allowed in target
return strings.Replace(target, `#`, `%23`, -1)
}