Skip to content
This repository was archived by the owner on Aug 13, 2025. It is now read-only.

Commit e41b457

Browse files
authored
Merge pull request #838 from Sajito/fix/hyperlinks-in-excel
2 parents 6c6b048 + c4c4d30 commit e41b457

File tree

7 files changed

+115
-32
lines changed

7 files changed

+115
-32
lines changed

cell.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,11 +376,11 @@ func (c *Cell) SetHyperlink(hyperlink string, displayText string, tooltip string
376376
h := strings.ToLower(hyperlink)
377377
if strings.HasPrefix(h, "http:") || strings.HasPrefix(h, "https://") {
378378
c.Hyperlink = Hyperlink{Link: hyperlink}
379+
c.Row.Sheet.addRelation(RelationshipTypeHyperlink, hyperlink, RelationshipTargetModeExternal)
379380
} else {
380-
c.Hyperlink = Hyperlink{Link: hyperlink, Location: hyperlink}
381+
c.Hyperlink = Hyperlink{Location: hyperlink}
381382
}
382383
c.SetString(hyperlink)
383-
c.Row.Sheet.addRelation(RelationshipTypeHyperlink, hyperlink, RelationshipTargetModeExternal)
384384
if displayText != "" {
385385
c.Hyperlink.DisplayString = displayText
386386
c.SetString(displayText)

diskv.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ func (dvr *DiskVRow) readCell(key string) (*Cell, error) {
124124
if c.Hyperlink.Link, err = readString(buf); err != nil {
125125
return c, err
126126
}
127+
if c.Hyperlink.Location, err = readString(buf); err != nil {
128+
return c, err
129+
}
127130
if c.Hyperlink.Tooltip, err = readString(buf); err != nil {
128131
return c, err
129132
}
@@ -198,6 +201,9 @@ func (dvr *DiskVRow) writeCell(c *Cell) error {
198201
if err = writeString(&dvr.buf, c.Hyperlink.Link); err != nil {
199202
return err
200203
}
204+
if err = writeString(&dvr.buf, c.Hyperlink.Location); err != nil {
205+
return err
206+
}
201207
if err = writeString(&dvr.buf, c.Hyperlink.Tooltip); err != nil {
202208
return err
203209
}
@@ -1074,6 +1080,9 @@ func writeCell(buf *bytes.Buffer, c *Cell) error {
10741080
if err = writeString(buf, c.Hyperlink.Link); err != nil {
10751081
return err
10761082
}
1083+
if err = writeString(buf, c.Hyperlink.Location); err != nil {
1084+
return err
1085+
}
10771086
if err = writeString(buf, c.Hyperlink.Tooltip); err != nil {
10781087
return err
10791088
}
@@ -1196,6 +1205,9 @@ func readCell(reader *bytes.Reader) (*Cell, error) {
11961205
if c.Hyperlink.Link, err = readString(reader); err != nil {
11971206
return c, err
11981207
}
1208+
if c.Hyperlink.Location, err = readString(reader); err != nil {
1209+
return c, err
1210+
}
11991211
if c.Hyperlink.Tooltip, err = readString(reader); err != nil {
12001212
return c, err
12011213
}

diskv_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ line!`)
454454
DisplayString: "displaystring",
455455
Link: "link",
456456
Tooltip: "tooltip",
457+
Location: "location",
457458
},
458459
num: 1,
459460
}

file_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"os"
77
"path/filepath"
8+
"strings"
89
"testing"
910

1011
qt "github.com/frankban/quicktest"
@@ -925,6 +926,27 @@ func TestFile(t *testing.T) {
925926
c.Assert(len(parts), qt.Equals, 13)
926927
})
927928

929+
csRunO(c, "TestMarshalFileWithInternalHyperlink", func(c *qt.C, option FileOption) {
930+
f := NewFile(option)
931+
sheet1, _ := f.AddSheet("MySheet")
932+
row1 := sheet1.AddRow()
933+
cell1 := row1.AddCell()
934+
cell1.SetString("A cell!")
935+
cell1.SetHyperlink("MySheet!A2", "Internal Link", "")
936+
c.Assert(cell1.Value, qt.Equals, "Internal Link")
937+
parts, err := f.MakeStreamParts()
938+
c.Assert(err, qt.IsNil)
939+
c.Assert(len(parts), qt.Equals, 10)
940+
// ensure internal hyperlink contains location
941+
c.Assert(parts["xl/worksheets/sheet1.xml"], qt.Contains, `<hyperlinks><hyperlink r:id="" ref="A1" display="Internal Link" location="MySheet!A2"></hyperlink></hyperlinks>`)
942+
943+
sdIndex := strings.Index(parts["xl/worksheets/sheet1.xml"], "</sheetData>")
944+
hIndex := strings.Index(parts["xl/worksheets/sheet1.xml"], "<hyperlinks>")
945+
if hIndex < sdIndex {
946+
c.Error("hyperlinks must come after sheetData")
947+
}
948+
})
949+
928950
csRunO(c, "TestMarshalFileWithHiddenSheet", func(c *qt.C, option FileOption) {
929951
f := NewFile(option)
930952
sheet1, _ := f.AddSheet("MySheet")

sheet.go

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -621,24 +621,34 @@ func (s *Sheet) prepWorksheetFromRows(worksheet *xlsxWorksheet, relations *xlsxW
621621
worksheet.Hyperlinks = &xlsxHyperlinks{HyperLinks: []xlsxHyperlink{}}
622622
}
623623

624-
var relId string
625-
if relations != nil && relations.Relationships != nil {
626-
for _, rel := range relations.Relationships {
627-
if rel.Target == cell.Hyperlink.Link {
628-
relId = rel.Id
624+
if cell.Hyperlink.Location != "" {
625+
xlsxLink := xlsxHyperlink{
626+
Reference: cellID,
627+
Location: cell.Hyperlink.Location,
628+
DisplayString: cell.Hyperlink.DisplayString,
629+
Tooltip: cell.Hyperlink.Tooltip}
630+
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
631+
} else {
632+
var relId string
633+
if relations != nil && relations.Relationships != nil {
634+
for _, rel := range relations.Relationships {
635+
if rel.Target == cell.Hyperlink.Link {
636+
relId = rel.Id
637+
}
629638
}
630639
}
631-
}
632640

633-
if relId != "" {
641+
if relId != "" {
634642

635-
xlsxLink := xlsxHyperlink{
636-
RelationshipId: relId,
637-
Reference: cellID,
638-
DisplayString: cell.Hyperlink.DisplayString,
639-
Tooltip: cell.Hyperlink.Tooltip}
640-
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
643+
xlsxLink := xlsxHyperlink{
644+
RelationshipId: relId,
645+
Reference: cellID,
646+
DisplayString: cell.Hyperlink.DisplayString,
647+
Tooltip: cell.Hyperlink.Tooltip}
648+
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
649+
}
641650
}
651+
642652
}
643653

644654
if cell.HMerge > 0 || cell.VMerge > 0 {
@@ -784,21 +794,30 @@ func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTa
784794
worksheet.Hyperlinks = &xlsxHyperlinks{HyperLinks: []xlsxHyperlink{}}
785795
}
786796

787-
var relId string
788-
for _, rel := range relations.Relationships {
789-
if rel.Target == cell.Hyperlink.Link {
790-
relId = rel.Id
797+
if cell.Hyperlink.Location != "" {
798+
xlsxLink := xlsxHyperlink{
799+
Reference: xC.R,
800+
Location: cell.Hyperlink.Location,
801+
DisplayString: cell.Hyperlink.DisplayString,
802+
Tooltip: cell.Hyperlink.Tooltip}
803+
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
804+
} else {
805+
var relId string
806+
for _, rel := range relations.Relationships {
807+
if rel.Target == cell.Hyperlink.Link {
808+
relId = rel.Id
809+
}
791810
}
792-
}
793811

794-
if relId != "" {
812+
if relId != "" {
795813

796-
xlsxLink := xlsxHyperlink{
797-
RelationshipId: relId,
798-
Reference: xC.R,
799-
DisplayString: cell.Hyperlink.DisplayString,
800-
Tooltip: cell.Hyperlink.Tooltip}
801-
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
814+
xlsxLink := xlsxHyperlink{
815+
RelationshipId: relId,
816+
Reference: xC.R,
817+
DisplayString: cell.Hyperlink.DisplayString,
818+
Tooltip: cell.Hyperlink.Tooltip}
819+
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
820+
}
802821
}
803822
}
804823

sheet_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,26 @@ func TestSheet(t *testing.T) {
334334
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheetPr filterMode="false"><pageSetUpPr fitToPage="false"/></sheetPr><dimension ref="A1:B1"/><sheetViews><sheetView windowProtection="false" showFormulas="false" showGridLines="true" showRowColHeaders="true" showZeros="true" rightToLeft="false" tabSelected="true" showOutlineSymbols="true" defaultGridColor="true" view="normal" topLeftCell="A1" colorId="64" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100" workbookViewId="0"><selection pane="topLeft" activeCell="A1" activeCellId="0" sqref="A1"/></sheetView></sheetViews><sheetFormatPr defaultRowHeight="12.85"/><sheetData><row r="1"><c r="A1" t="s"><v>0</v></c><c r="B1" t="s"><v>1</v></c></row></sheetData></worksheet>`
335335
c.Assert(buf.String(), qt.Equals, expectedXLSXSheet)
336336
})
337+
csRunO(c, "TestMarshalSheetWithInternalLinks", func(c *qt.C, option FileOption) {
338+
file := NewFile(option)
339+
sheet, _ := file.AddSheet("Sheet1")
340+
row := sheet.AddRow()
341+
cell := row.AddCell()
342+
cell.SetValue("First cell")
343+
cell = row.AddCell()
344+
cell.SetHyperlink("Sheet1!A1", "Link to first", "")
345+
var buf bytes.Buffer
346+
347+
refTable := NewSharedStringRefTable(2)
348+
styles := newXlsxStyleSheet(nil)
349+
err := sheet.MarshalSheet(&buf, refTable, styles, nil)
350+
c.Assert(err, qt.IsNil)
351+
352+
expectedXLSXSheet := `<?xml version="1.0" encoding="UTF-8"?>
353+
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheetPr filterMode="false"><pageSetUpPr fitToPage="false"/></sheetPr><dimension ref="A1:B1"/><sheetViews><sheetView windowProtection="false" showFormulas="false" showGridLines="true" showRowColHeaders="true" showZeros="true" rightToLeft="false" tabSelected="true" showOutlineSymbols="true" defaultGridColor="true" view="normal" topLeftCell="A1" colorId="64" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100" workbookViewId="0"><selection pane="topLeft" activeCell="A1" activeCellId="0" sqref="A1"/></sheetView></sheetViews><sheetFormatPr defaultRowHeight="12.85"/><sheetData><row r="1"><c r="A1" t="s"><v>0</v></c><c r="B1" t="s"><v>1</v></c></row></sheetData><hyperlinks><hyperlink r:id="" ref="B1" display="Link to first" location="Sheet1!A1"/></hyperlinks></worksheet>`
354+
s := buf.String()
355+
c.Assert(s, qt.Equals, expectedXLSXSheet)
356+
})
337357
csRunO(c, "TestSetRowHeightCM", func(c *qt.C, option FileOption) {
338358
file := NewFile(option)
339359
sheet, _ := file.AddSheet("Sheet1")

xmlWorksheet.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ type xlsxDataValidations struct {
270270
// The list validation type would more commonly be called "a drop down box."
271271
type xlsxDataValidation struct {
272272
// A boolean value indicating whether the data validation allows the use of empty or blank
273-
//entries. 1 means empty entries are OK and do not violate the validation constraints.
273+
// entries. 1 means empty entries are OK and do not violate the validation constraints.
274274
AllowBlank bool `xml:"allowBlank,attr,omitempty"`
275275
// A boolean value indicating whether to display the input prompt message.
276276
ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"`
@@ -595,7 +595,7 @@ func emitStructAsXML(v reflect.Value, name, xmlNS string) (xmlwriter.Elem, error
595595
Name: "xmlns",
596596
Value: xmlNS,
597597
})
598-
case "SheetData", "MergeCells", "DataValidations", "AutoFilter":
598+
case "SheetData", "MergeCells", "DataValidations", "AutoFilter", "Hyperlinks":
599599
// Skip SheetData here, we explicitly generate this in writeXML below
600600
// Microsoft Excel considers a mergeCells element before a sheetData element to be
601601
// an error and will fail to open the document, so we'll be back with this data
@@ -755,6 +755,15 @@ func (worksheet *xlsxWorksheet) WriteXML(xw *xmlwriter.Writer, s *Sheet, styles
755755
}, SkipEmptyRows),
756756
xw.EndElem("sheetData"),
757757
func() error {
758+
if worksheet.AutoFilter != nil {
759+
autoFilter, err := emitStructAsXML(reflect.ValueOf(worksheet.AutoFilter), "autoFilter", "")
760+
if err != nil {
761+
return err
762+
}
763+
if err := xw.Write(autoFilter); err != nil {
764+
return err
765+
}
766+
}
758767
if worksheet.MergeCells != nil {
759768
mergeCells, err := emitStructAsXML(reflect.ValueOf(worksheet.MergeCells), "mergeCells", "")
760769
if err != nil {
@@ -773,12 +782,12 @@ func (worksheet *xlsxWorksheet) WriteXML(xw *xmlwriter.Writer, s *Sheet, styles
773782
return err
774783
}
775784
}
776-
if worksheet.AutoFilter != nil {
777-
autoFilter, err := emitStructAsXML(reflect.ValueOf(worksheet.AutoFilter), "autoFilter", "")
785+
if worksheet.Hyperlinks != nil {
786+
hyperlinks, err := emitStructAsXML(reflect.ValueOf(worksheet.Hyperlinks), "hyperlinks", "")
778787
if err != nil {
779788
return err
780789
}
781-
if err := xw.Write(autoFilter); err != nil {
790+
if err := xw.Write(hyperlinks); err != nil {
782791
return err
783792
}
784793
}

0 commit comments

Comments
 (0)