Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix hyperlinks in excel #838

Merged
merged 2 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,11 +376,11 @@ func (c *Cell) SetHyperlink(hyperlink string, displayText string, tooltip string
h := strings.ToLower(hyperlink)
if strings.HasPrefix(h, "http:") || strings.HasPrefix(h, "https://") {
c.Hyperlink = Hyperlink{Link: hyperlink}
c.Row.Sheet.addRelation(RelationshipTypeHyperlink, hyperlink, RelationshipTargetModeExternal)
} else {
c.Hyperlink = Hyperlink{Link: hyperlink, Location: hyperlink}
c.Hyperlink = Hyperlink{Location: hyperlink}
}
c.SetString(hyperlink)
c.Row.Sheet.addRelation(RelationshipTypeHyperlink, hyperlink, RelationshipTargetModeExternal)
if displayText != "" {
c.Hyperlink.DisplayString = displayText
c.SetString(displayText)
Expand Down
12 changes: 12 additions & 0 deletions diskv.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ func (dvr *DiskVRow) readCell(key string) (*Cell, error) {
if c.Hyperlink.Link, err = readString(buf); err != nil {
return c, err
}
if c.Hyperlink.Location, err = readString(buf); err != nil {
return c, err
}
if c.Hyperlink.Tooltip, err = readString(buf); err != nil {
return c, err
}
Expand Down Expand Up @@ -198,6 +201,9 @@ func (dvr *DiskVRow) writeCell(c *Cell) error {
if err = writeString(&dvr.buf, c.Hyperlink.Link); err != nil {
return err
}
if err = writeString(&dvr.buf, c.Hyperlink.Location); err != nil {
return err
}
if err = writeString(&dvr.buf, c.Hyperlink.Tooltip); err != nil {
return err
}
Expand Down Expand Up @@ -1074,6 +1080,9 @@ func writeCell(buf *bytes.Buffer, c *Cell) error {
if err = writeString(buf, c.Hyperlink.Link); err != nil {
return err
}
if err = writeString(buf, c.Hyperlink.Location); err != nil {
return err
}
if err = writeString(buf, c.Hyperlink.Tooltip); err != nil {
return err
}
Expand Down Expand Up @@ -1196,6 +1205,9 @@ func readCell(reader *bytes.Reader) (*Cell, error) {
if c.Hyperlink.Link, err = readString(reader); err != nil {
return c, err
}
if c.Hyperlink.Location, err = readString(reader); err != nil {
return c, err
}
if c.Hyperlink.Tooltip, err = readString(reader); err != nil {
return c, err
}
Expand Down
1 change: 1 addition & 0 deletions diskv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ line!`)
DisplayString: "displaystring",
Link: "link",
Tooltip: "tooltip",
Location: "location",
},
num: 1,
}
Expand Down
22 changes: 22 additions & 0 deletions file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"os"
"path/filepath"
"strings"
"testing"

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

csRunO(c, "TestMarshalFileWithInternalHyperlink", func(c *qt.C, option FileOption) {
f := NewFile(option)
sheet1, _ := f.AddSheet("MySheet")
row1 := sheet1.AddRow()
cell1 := row1.AddCell()
cell1.SetString("A cell!")
cell1.SetHyperlink("MySheet!A2", "Internal Link", "")
c.Assert(cell1.Value, qt.Equals, "Internal Link")
parts, err := f.MakeStreamParts()
c.Assert(err, qt.IsNil)
c.Assert(len(parts), qt.Equals, 10)
// ensure internal hyperlink contains location
c.Assert(parts["xl/worksheets/sheet1.xml"], qt.Contains, `<hyperlinks><hyperlink r:id="" ref="A1" display="Internal Link" location="MySheet!A2"></hyperlink></hyperlinks>`)

sdIndex := strings.Index(parts["xl/worksheets/sheet1.xml"], "</sheetData>")
hIndex := strings.Index(parts["xl/worksheets/sheet1.xml"], "<hyperlinks>")
if hIndex < sdIndex {
c.Error("hyperlinks must come after sheetData")
}
})

csRunO(c, "TestMarshalFileWithHiddenSheet", func(c *qt.C, option FileOption) {
f := NewFile(option)
sheet1, _ := f.AddSheet("MySheet")
Expand Down
69 changes: 44 additions & 25 deletions sheet.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,24 +621,34 @@ func (s *Sheet) prepWorksheetFromRows(worksheet *xlsxWorksheet, relations *xlsxW
worksheet.Hyperlinks = &xlsxHyperlinks{HyperLinks: []xlsxHyperlink{}}
}

var relId string
if relations != nil && relations.Relationships != nil {
for _, rel := range relations.Relationships {
if rel.Target == cell.Hyperlink.Link {
relId = rel.Id
if cell.Hyperlink.Location != "" {
xlsxLink := xlsxHyperlink{
Reference: cellID,
Location: cell.Hyperlink.Location,
DisplayString: cell.Hyperlink.DisplayString,
Tooltip: cell.Hyperlink.Tooltip}
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
} else {
var relId string
if relations != nil && relations.Relationships != nil {
for _, rel := range relations.Relationships {
if rel.Target == cell.Hyperlink.Link {
relId = rel.Id
}
}
}
}

if relId != "" {
if relId != "" {

xlsxLink := xlsxHyperlink{
RelationshipId: relId,
Reference: cellID,
DisplayString: cell.Hyperlink.DisplayString,
Tooltip: cell.Hyperlink.Tooltip}
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
xlsxLink := xlsxHyperlink{
RelationshipId: relId,
Reference: cellID,
DisplayString: cell.Hyperlink.DisplayString,
Tooltip: cell.Hyperlink.Tooltip}
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
}
}

}

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

var relId string
for _, rel := range relations.Relationships {
if rel.Target == cell.Hyperlink.Link {
relId = rel.Id
if cell.Hyperlink.Location != "" {
xlsxLink := xlsxHyperlink{
Reference: xC.R,
Location: cell.Hyperlink.Location,
DisplayString: cell.Hyperlink.DisplayString,
Tooltip: cell.Hyperlink.Tooltip}
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
} else {
var relId string
for _, rel := range relations.Relationships {
if rel.Target == cell.Hyperlink.Link {
relId = rel.Id
}
}
}

if relId != "" {
if relId != "" {

xlsxLink := xlsxHyperlink{
RelationshipId: relId,
Reference: xC.R,
DisplayString: cell.Hyperlink.DisplayString,
Tooltip: cell.Hyperlink.Tooltip}
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
xlsxLink := xlsxHyperlink{
RelationshipId: relId,
Reference: xC.R,
DisplayString: cell.Hyperlink.DisplayString,
Tooltip: cell.Hyperlink.Tooltip}
worksheet.Hyperlinks.HyperLinks = append(worksheet.Hyperlinks.HyperLinks, xlsxLink)
}
}
}

Expand Down
20 changes: 20 additions & 0 deletions sheet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,26 @@ func TestSheet(t *testing.T) {
<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>`
c.Assert(buf.String(), qt.Equals, expectedXLSXSheet)
})
csRunO(c, "TestMarshalSheetWithInternalLinks", func(c *qt.C, option FileOption) {
file := NewFile(option)
sheet, _ := file.AddSheet("Sheet1")
row := sheet.AddRow()
cell := row.AddCell()
cell.SetValue("First cell")
cell = row.AddCell()
cell.SetHyperlink("Sheet1!A1", "Link to first", "")
var buf bytes.Buffer

refTable := NewSharedStringRefTable(2)
styles := newXlsxStyleSheet(nil)
err := sheet.MarshalSheet(&buf, refTable, styles, nil)
c.Assert(err, qt.IsNil)

expectedXLSXSheet := `<?xml version="1.0" encoding="UTF-8"?>
<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>`
s := buf.String()
c.Assert(s, qt.Equals, expectedXLSXSheet)
})
csRunO(c, "TestSetRowHeightCM", func(c *qt.C, option FileOption) {
file := NewFile(option)
sheet, _ := file.AddSheet("Sheet1")
Expand Down
19 changes: 14 additions & 5 deletions xmlWorksheet.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ type xlsxDataValidations struct {
// The list validation type would more commonly be called "a drop down box."
type xlsxDataValidation struct {
// A boolean value indicating whether the data validation allows the use of empty or blank
//entries. 1 means empty entries are OK and do not violate the validation constraints.
// entries. 1 means empty entries are OK and do not violate the validation constraints.
AllowBlank bool `xml:"allowBlank,attr,omitempty"`
// A boolean value indicating whether to display the input prompt message.
ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"`
Expand Down Expand Up @@ -595,7 +595,7 @@ func emitStructAsXML(v reflect.Value, name, xmlNS string) (xmlwriter.Elem, error
Name: "xmlns",
Value: xmlNS,
})
case "SheetData", "MergeCells", "DataValidations", "AutoFilter":
case "SheetData", "MergeCells", "DataValidations", "AutoFilter", "Hyperlinks":
// Skip SheetData here, we explicitly generate this in writeXML below
// Microsoft Excel considers a mergeCells element before a sheetData element to be
// an error and will fail to open the document, so we'll be back with this data
Expand Down Expand Up @@ -755,6 +755,15 @@ func (worksheet *xlsxWorksheet) WriteXML(xw *xmlwriter.Writer, s *Sheet, styles
}, SkipEmptyRows),
xw.EndElem("sheetData"),
func() error {
if worksheet.AutoFilter != nil {
autoFilter, err := emitStructAsXML(reflect.ValueOf(worksheet.AutoFilter), "autoFilter", "")
if err != nil {
return err
}
if err := xw.Write(autoFilter); err != nil {
return err
}
}
if worksheet.MergeCells != nil {
mergeCells, err := emitStructAsXML(reflect.ValueOf(worksheet.MergeCells), "mergeCells", "")
if err != nil {
Expand All @@ -773,12 +782,12 @@ func (worksheet *xlsxWorksheet) WriteXML(xw *xmlwriter.Writer, s *Sheet, styles
return err
}
}
if worksheet.AutoFilter != nil {
autoFilter, err := emitStructAsXML(reflect.ValueOf(worksheet.AutoFilter), "autoFilter", "")
if worksheet.Hyperlinks != nil {
hyperlinks, err := emitStructAsXML(reflect.ValueOf(worksheet.Hyperlinks), "hyperlinks", "")
if err != nil {
return err
}
if err := xw.Write(autoFilter); err != nil {
if err := xw.Write(hyperlinks); err != nil {
return err
}
}
Expand Down