c5_labsci/zciyon/xlsx/sheet.go
2026-01-27 00:52:00 +08:00

953 lines
26 KiB
Go

package xlsx
import (
"encoding/xml"
"errors"
"fmt"
"io"
"strconv"
"strings"
"unicode/utf8"
)
// Sheet is a high level structure intended to provide user access to
// the contents of a particular sheet within an XLSX file.
type Sheet struct {
Name string
File *File
Cols *ColStore
MaxRow int
MaxCol int
Hidden bool
Selected bool
SheetViews []SheetView
SheetFormat SheetFormat
AutoFilter *AutoFilter
Relations []Relation
DataValidations []*xlsxDataValidation
cellStore CellStore
currentRow *Row
cellStoreName string // The first part of the key used in
// the cellStore. This name is stable,
// unlike the Name, which can change
}
// NewSheet constructs a Sheet with the default CellStore and returns
// a pointer to it.
func NewSheet(name string) (*Sheet, error) {
return NewSheetWithCellStore(name, NewMemoryCellStore)
}
// NewSheetWithCellStore constructs a Sheet, backed by a CellStore,
// for which you must provide the constructor function.
func NewSheetWithCellStore(name string, constructor CellStoreConstructor) (*Sheet, error) {
if err := IsSaneSheetName(name); err != nil {
return nil, fmt.Errorf("sheet name is invalid: %w", err)
}
sheet := &Sheet{
Name: name,
Cols: &ColStore{},
cellStoreName: name,
}
var err error
sheet.cellStore, err = constructor()
if err != nil {
return nil, fmt.Errorf("NewSheetWithCellStore: %w", err)
}
return sheet, err
}
// Remove Sheet's dependant resources - if you are done with operations on a sheet this should be called to clear down the Sheet's persistent cache. Note: if you call this, all further read operaton on the sheet will fail - including any attempt to save the file, or dump it's contents to a byte stream. Therefore only call this *after* you've saved your changes, of when you're done reading a sheet in a file you don't plan to persist.
func (s *Sheet) Close() {
s.cellStore.Close()
s.cellStore = nil
}
func (s *Sheet) getState() string {
if s.Hidden {
return "hidden"
}
return "visible"
}
type SheetView struct {
Pane *Pane
}
type Pane struct {
XSplit float64
YSplit float64
TopLeftCell string
ActivePane string
State string // Either "split" or "frozen"
}
type SheetFormat struct {
DefaultColWidth float64
DefaultRowHeight float64
OutlineLevelCol uint8
OutlineLevelRow uint8
}
type AutoFilter struct {
TopLeftCell string
BottomRightCell string
}
type Relation struct {
Type RelationshipType
Target string
TargetMode RelationshipTargetMode
}
func (s *Sheet) makeXLSXSheetRelations() *xlsxWorksheetRels {
relSheet := xlsxWorksheetRels{XMLName: xml.Name{Local: "Relationships"}, Relationships: []xlsxWorksheetRelation{}}
for id, rel := range s.Relations {
xRel := xlsxWorksheetRelation{Id: "rId" + strconv.Itoa(id+1), Type: rel.Type, Target: rel.Target, TargetMode: rel.TargetMode}
relSheet.Relationships = append(relSheet.Relationships, xRel)
}
if len(relSheet.Relationships) == 0 {
return nil
}
return &relSheet
}
func (s *Sheet) addRelation(relType RelationshipType, target string, targetMode RelationshipTargetMode) {
newRel := Relation{Type: relType, Target: target, TargetMode: targetMode}
for _, rel := range s.Relations {
if rel == newRel {
return
}
}
s.Relations = append(s.Relations, newRel)
}
func (s *Sheet) setCurrentRow(r *Row) {
if r != nil && r == s.currentRow {
return
}
if s.currentRow != nil && s.currentRow.isCustom {
err := s.cellStore.WriteRow(s.currentRow)
if err != nil {
panic(err)
}
}
s.currentRow = r
}
// rowVisitorFlags contains flags that can be set by a RowVisitorOption to affect the behaviour of sheet.ForEachRow
type rowVisitorFlags struct {
skipEmptyRows bool
}
// RowVisitorOption defines the call signature of functions that can be passed as options to the Sheet.ForEachRow function to affect its behaviour.
type RowVisitorOption func(flags *rowVisitorFlags)
// SkipEmptyRows can be passed to the Sheet.ForEachRow function to
// cause it to skip over empty Rows.
func SkipEmptyRows(flags *rowVisitorFlags) {
flags.skipEmptyRows = true
}
// A RowVisitor function should be provided by the user when calling
// Sheet.ForEachRow, it will be called once for every Row visited.
type RowVisitor func(r *Row) error
func (s *Sheet) mustBeOpen() {
if s.cellStore == nil {
panic("Attempt to iterate over sheet with no cellstore. Perhaps you called Close() on this sheet?")
}
}
func (s *Sheet) ForEachRow(rv RowVisitor, options ...RowVisitorOption) error {
s.mustBeOpen()
flags := &rowVisitorFlags{}
for _, opt := range options {
opt(flags)
}
if s.currentRow != nil {
err := s.cellStore.WriteRow(s.currentRow)
if err != nil {
return err
}
}
for i := 0; i < s.MaxRow; i++ {
r, err := s.cellStore.ReadRow(makeRowKey(s, i), s)
if err != nil {
if _, ok := err.(*RowNotFoundError); !ok {
return err
}
if flags.skipEmptyRows {
continue
}
r = s.cellStore.MakeRow(s)
r.num = i
}
if r.cellStoreRow.CellCount() == 0 && flags.skipEmptyRows {
continue
}
r.Sheet = s
s.setCurrentRow(r)
err = rv(r)
if err != nil {
return err
}
}
return nil
}
// Add a new Row to a Sheet
func (s *Sheet) AddRow() *Row {
s.mustBeOpen()
// NOTE - this is not safe to use concurrently
if s.currentRow != nil {
s.cellStore.WriteRow(s.currentRow)
}
row := s.cellStore.MakeRow(s)
row.num = s.MaxRow
s.MaxRow++
s.setCurrentRow(row)
return row
}
func makeRowKey(s *Sheet, i int) string {
return fmt.Sprintf("%s:%06d", s.cellStoreName, i)
}
// Add a new Row to a Sheet at a specific index
func (s *Sheet) AddRowAtIndex(index int) (*Row, error) {
s.mustBeOpen()
if index < 0 || index > s.MaxRow {
return nil, errors.New("AddRowAtIndex: index out of bounds")
}
if s.currentRow != nil {
s.cellStore.WriteRow(s.currentRow)
}
// We move rows in reverse order to avoid overwriting anyting
for i := (s.MaxRow - 1); i >= index; i-- {
nRow, err := s.cellStore.ReadRow(makeRowKey(s, i), s)
if err != nil {
continue
}
nRow.Sheet = s
s.setCurrentRow(nRow)
s.cellStore.MoveRow(nRow, i+1)
}
row := s.cellStore.MakeRow(s)
row.num = index
s.setCurrentRow(row)
err := s.cellStore.WriteRow(row)
if err != nil {
return nil, err
}
s.MaxRow++
return row, nil
}
// Add a DataValidation to a range of cells
func (s *Sheet) AddDataValidation(dv *xlsxDataValidation) {
s.mustBeOpen()
s.DataValidations = append(s.DataValidations, dv)
}
// Removes a row at a specific index
func (s *Sheet) RemoveRowAtIndex(index int) error {
s.mustBeOpen()
if index < 0 || index >= s.MaxRow {
return fmt.Errorf("Cannot remove row: index out of range: %d", index)
}
if s.currentRow != nil {
s.setCurrentRow(nil)
}
err := s.cellStore.RemoveRow(makeRowKey(s, index))
if err != nil {
return err
}
for i := index + 1; i < s.MaxRow; i++ {
nRow, err := s.cellStore.ReadRow(makeRowKey(s, i), s)
if err != nil {
continue
}
nRow.Sheet = s
s.cellStore.MoveRow(nRow, i-1)
}
s.MaxRow--
return nil
}
// Make sure we always have as many Rows as we do cells.
func (s *Sheet) maybeAddRow(rowCount int) {
s.mustBeOpen()
if rowCount > s.MaxRow {
loopCnt := rowCount - s.MaxRow
for i := 0; i < loopCnt; i++ {
row := s.cellStore.MakeRow(s)
row.num = i
s.setCurrentRow(row)
}
s.MaxRow = rowCount
}
}
// Make sure we always have as many Rows as we do cells.
func (s *Sheet) Row(idx int) (*Row, error) {
s.mustBeOpen()
s.maybeAddRow(idx + 1)
if s.currentRow != nil && idx == s.currentRow.num {
return s.currentRow, nil
}
r, err := s.cellStore.ReadRow(makeRowKey(s, idx), s)
if err != nil {
if _, ok := err.(*RowNotFoundError); !ok {
return nil, err
}
}
if r == nil {
r = s.cellStore.MakeRow(s)
r.num = idx
} else {
r.Sheet = s
}
s.setCurrentRow(r)
return r, nil
}
// Return the Col that applies to this Column index, or return nil if no such Col exists
// Column numbers start from 1.
func (s *Sheet) Col(idx int) *Col {
s.mustBeOpen()
if s.Cols == nil {
panic("trying to use uninitialised ColStore")
}
return s.Cols.FindColByIndex(idx + 1)
}
// Get a Cell by passing it's cartesian coordinates (zero based) as
// row and column integer indexes.
//
// For example:
//
// cell := sheet.Cell(0,0)
//
// ... would set the variable "cell" to contain a Cell struct
// containing the data from the field "A1" on the spreadsheet.
func (s *Sheet) Cell(row, col int) (*Cell, error) {
s.mustBeOpen()
// If the user requests a row beyond what we have, then extend.
for s.MaxRow <= row {
s.AddRow()
}
r, err := s.Row(row)
if err != nil {
return nil, err
}
cell := r.GetCell(col)
cell.Row = r
return cell, err
}
// Set the parameters of a column. Parameters are passed as a pointer
// to a Col structure which you much construct yourself.
// Column numbers start from 1.
func (s *Sheet) SetColParameters(col *Col) {
s.mustBeOpen()
if s.Cols == nil {
panic("trying to use uninitialised ColStore")
}
s.Cols.Add(col)
}
func (s *Sheet) setCol(min, max int, setter func(col *Col)) {
s.mustBeOpen()
if s.Cols == nil {
panic("trying to use uninitialised ColStore")
}
cols := s.Cols.getOrMakeColsForRange(s.Cols.Root, min, max)
for _, col := range cols {
switch {
case col.Min < min && col.Max > max:
// The column completely envelops the range,
// so we'll split it into three parts and only
// set the width on the part within the range.
// The ColStore will do most of this work for
// us, we just need to create the new Col
// based on the old one.
newCol := col.copyToRange(min, max)
setter(newCol)
s.Cols.Add(newCol)
case col.Min < min:
// If this column crosses the minimum boundary
// of the range we must split it and only
// apply the change within the range. Again,
// we can lean on the ColStore to deal with
// the rest we just need to make the new
// Col.
newCol := col.copyToRange(min, col.Max)
setter(newCol)
s.Cols.Add(newCol)
case col.Max > max:
// Likewise if a col definition crosses the
// maximum boundary of the range, it must also
// be split
newCol := col.copyToRange(col.Min, max)
setter(newCol)
s.Cols.Add(newCol)
default:
newCol := col.copyToRange(min, max)
setter(newCol)
s.Cols.Add(newCol)
}
}
return
}
// Set the width of a range of columns.
// Column numbers start from 1.
func (s *Sheet) SetColWidth(min, max int, width float64) {
s.mustBeOpen()
s.setCol(min, max, func(col *Col) {
col.SetWidth(width)
})
}
// This can be use as the default scale function for the autowidth.
// It works well with the default font sizes.
func DefaultAutoWidth(s string) float64 {
return (float64(strings.Count(s, "")) + 3.0) * 1.2
}
// Tries to guess the best width for a column, based on the largest
// cell content. A scale function needs to be provided.
// Column numbers start from 1.
func (s *Sheet) SetColAutoWidth(colIndex int, width func(string) float64) error {
s.mustBeOpen()
largestWidth := 0.0
rowVisitor := func(r *Row) error {
cell := r.GetCell(colIndex - 1)
value, err := cell.FormattedValue()
if err != nil {
return err
}
if width(value) > largestWidth {
largestWidth = width(value)
}
return nil
}
err := s.ForEachRow(rowVisitor)
if err != nil {
return err
}
s.SetColWidth(colIndex, colIndex, largestWidth)
return nil
}
// Set the outline level for a range of columns.
func (s *Sheet) SetOutlineLevel(minCol, maxCol int, outlineLevel uint8) {
s.mustBeOpen()
s.setCol(minCol, maxCol, func(col *Col) {
col.SetOutlineLevel(outlineLevel)
})
}
// Set the type for a range of columns.
func (s *Sheet) SetType(minCol, maxCol int, cellType CellType) {
s.mustBeOpen()
s.setCol(minCol, maxCol, func(col *Col) {
col.SetType(cellType)
})
}
// When merging cells, the cell may be the 'original' or the 'covered'.
// First, figure out which cells are merge starting points. Then create
// the necessary cells underlying the merge area.
// Then go through all the underlying cells and apply the appropriate
// border, based on the original cell.
func (s *Sheet) handleMerged() {
merged := make(map[string]*Cell)
s.ForEachRow(func(row *Row) error {
return row.ForEachCell(func(cell *Cell) error {
if cell.HMerge > 0 || cell.VMerge > 0 {
coord := GetCellIDStringFromCoords(cell.num, row.num)
merged[coord] = cell
}
return nil
}, SkipEmptyCells)
}, SkipEmptyRows)
// This loop iterates over all cells that should be merged and applies the correct
// borders to them depending on their position. If any cells required by the merge
// are missing, they will be allocated by s.Cell().
for key, cell := range merged {
maincol, mainrow, _ := GetCoordsFromCellIDString(key)
for rownum := 0; rownum <= cell.VMerge; rownum++ {
for colnum := 0; colnum <= cell.HMerge; colnum++ {
// make cell
s.Cell(mainrow+rownum, maincol+colnum)
}
}
}
}
func (s *Sheet) makeSheetView(worksheet *xlsxWorksheet) {
for index, sheetView := range s.SheetViews {
if sheetView.Pane != nil {
worksheet.SheetViews.SheetView[index].Pane = &xlsxPane{
XSplit: sheetView.Pane.XSplit,
YSplit: sheetView.Pane.YSplit,
TopLeftCell: sheetView.Pane.TopLeftCell,
ActivePane: sheetView.Pane.ActivePane,
State: sheetView.Pane.State,
}
}
}
if s.Selected {
worksheet.SheetViews.SheetView[0].TabSelected = true
}
}
func (s *Sheet) makeSheetFormatPr(worksheet *xlsxWorksheet) {
if s.SheetFormat.DefaultRowHeight != 0 {
worksheet.SheetFormatPr.DefaultRowHeight = s.SheetFormat.DefaultRowHeight
}
worksheet.SheetFormatPr.DefaultColWidth = s.SheetFormat.DefaultColWidth
}
func (s *Sheet) makeCols(worksheet *xlsxWorksheet, styles *xlsxStyleSheet) (maxLevelCol uint8) {
s.mustBeOpen()
maxLevelCol = 0
if s.Cols == nil {
panic("trying to use uninitialised ColStore")
}
s.Cols.ForEach(
func(c int, col *Col) {
XfId := 0
style := col.GetStyle()
hasNumFmt := len(col.numFmt) > 0
if hasNumFmt {
if style == nil {
style = NewStyle()
}
xNumFmt := styles.newNumFmt(col.numFmt)
XfId = handleStyleForXLSX(style, xNumFmt.NumFmtId, styles)
} else {
if style != nil {
XfId = handleStyleForXLSX(style, 0, styles)
}
}
col.outXfID = XfId
// When the cols content is empty, the cols flag is not output in the xml file.
if worksheet.Cols == nil {
worksheet.Cols = &xlsxCols{Col: []xlsxCol{}}
}
worksheet.Cols.Col = append(worksheet.Cols.Col,
xlsxCol{
Min: col.Min,
Max: col.Max,
Hidden: col.Hidden,
Width: col.Width,
CustomWidth: col.CustomWidth,
Collapsed: col.Collapsed,
OutlineLevel: col.OutlineLevel,
Style: &XfId,
BestFit: col.BestFit,
Phonetic: col.Phonetic,
})
if col.OutlineLevel != nil && *col.OutlineLevel > maxLevelCol {
maxLevelCol = *col.OutlineLevel
}
})
return maxLevelCol
}
func (s *Sheet) prepSheetForMarshalling(maxLevelCol uint8) {
s.SheetFormat.OutlineLevelCol = maxLevelCol
}
func (s *Sheet) prepWorksheetFromRows(worksheet *xlsxWorksheet, relations *xlsxWorksheetRels) error {
s.mustBeOpen()
var maxCell, maxRow int
prepRow := func(row *Row) error {
if row.num > maxRow {
maxRow = row.num
}
prepCell := func(cell *Cell) error {
if cell.num > maxCell {
maxCell = cell.num
}
cellID := GetCellIDStringFromCoords(cell.num, row.num)
if nil != cell.DataValidation {
if nil == worksheet.DataValidations {
worksheet.DataValidations = &xlsxDataValidations{}
}
cell.DataValidation.Sqref = cellID
worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation, cell.DataValidation)
worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation)
}
if cell.Hyperlink != (Hyperlink{}) {
if worksheet.Hyperlinks == nil {
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 relId != "" {
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 {
mc := xlsxMergeCell{}
start := fmt.Sprintf("%s%d", ColIndexToLetters(cell.num), row.num+1)
endcol := cell.num + cell.HMerge
endrow := row.num + cell.VMerge + 1
end := fmt.Sprintf("%s%d", ColIndexToLetters(endcol), endrow)
mc.Ref = start + ":" + end
if worksheet.MergeCells == nil {
worksheet.MergeCells = &xlsxMergeCells{}
}
worksheet.MergeCells.Cells = append(worksheet.MergeCells.Cells, mc)
worksheet.MergeCells.addCell(mc)
}
return nil
}
return row.ForEachCell(prepCell, SkipEmptyCells)
}
err := s.ForEachRow(prepRow, SkipEmptyRows)
if err != nil {
return err
}
worksheet.SheetFormatPr.OutlineLevelCol = s.SheetFormat.OutlineLevelCol
worksheet.SheetFormatPr.OutlineLevelRow = s.SheetFormat.OutlineLevelRow
if worksheet.MergeCells != nil {
worksheet.MergeCells.Count = len(worksheet.MergeCells.Cells)
}
if s.AutoFilter != nil {
worksheet.AutoFilter = &xlsxAutoFilter{Ref: fmt.Sprintf("%v:%v", s.AutoFilter.TopLeftCell, s.AutoFilter.BottomRightCell)}
}
dimension := xlsxDimension{}
dimension.Ref = "A1:" + GetCellIDStringFromCoords(maxCell, maxRow)
if dimension.Ref == "A1:A1" {
dimension.Ref = "A1"
}
worksheet.Dimension = dimension
return nil
}
func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTable *RefTable, relations *xlsxWorksheetRels, maxLevelCol uint8) error {
s.mustBeOpen()
maxRow := 0
maxCell := 0
var maxLevelRow uint8
xSheet := xlsxSheetData{}
makeR := func(row *Row) error {
r := row.num
if r > maxRow {
maxRow = r
}
xRow := xlsxRow{}
xRow.R = r + 1
if row.customHeight {
xRow.CustomHeight = true
xRow.Ht = fmt.Sprintf("%g", row.GetHeight())
}
xRow.OutlineLevel = row.GetOutlineLevel()
if xRow.OutlineLevel > maxLevelRow {
maxLevelRow = xRow.OutlineLevel
}
makeC := func(cell *Cell) error {
var XfId int
c := cell.num
col := s.Col(c)
if col != nil {
XfId = col.outXfID
}
// generate NumFmtId and add new NumFmt
xNumFmt := styles.newNumFmt(cell.NumFmt)
style := cell.style
switch {
case style != nil:
XfId = handleStyleForXLSX(style, xNumFmt.NumFmtId, styles)
case len(cell.NumFmt) == 0:
// Do nothing
case col == nil:
XfId = handleNumFmtIdForXLSX(xNumFmt.NumFmtId, styles)
case !compareFormatString(col.numFmt, cell.NumFmt):
XfId = handleNumFmtIdForXLSX(xNumFmt.NumFmtId, styles)
}
if c > maxCell {
maxCell = c
}
xC := xlsxC{
S: XfId,
R: GetCellIDStringFromCoords(c, r),
}
if cell.formula != "" {
xC.F = &xlsxF{Content: cell.formula}
}
switch cell.cellType {
case CellTypeInline:
// Inline strings are turned into shared strings since they are more efficient.
// This is what Excel does as well.
fallthrough
case CellTypeString:
if len(cell.Value) > 0 {
xC.V = strconv.Itoa(refTable.AddString(cell.Value))
} else if len(cell.RichText) > 0 {
xC.V = strconv.Itoa(refTable.AddRichText(cell.RichText))
}
xC.T = "s"
case CellTypeNumeric:
// Numeric is the default, so the type can be left blank
xC.V = cell.Value
case CellTypeBool:
xC.V = cell.Value
xC.T = "b"
case CellTypeError:
xC.V = cell.Value
xC.T = "e"
case CellTypeDate:
xC.V = cell.Value
xC.T = "d"
case CellTypeStringFormula:
xC.V = cell.Value
xC.T = "str"
default:
panic(errors.New("unknown cell type cannot be marshaled"))
}
xRow.C = append(xRow.C, xC)
if nil != cell.DataValidation {
if nil == worksheet.DataValidations {
worksheet.DataValidations = &xlsxDataValidations{}
}
cell.DataValidation.Sqref = xC.R
worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation, cell.DataValidation)
worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation)
}
if cell.Hyperlink != (Hyperlink{}) {
if worksheet.Hyperlinks == nil {
worksheet.Hyperlinks = &xlsxHyperlinks{HyperLinks: []xlsxHyperlink{}}
}
var relId string
for _, rel := range relations.Relationships {
if rel.Target == cell.Hyperlink.Link {
relId = rel.Id
}
}
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)
}
}
if cell.HMerge > 0 || cell.VMerge > 0 {
// r == rownum, c == colnum
mc := xlsxMergeCell{}
start := fmt.Sprintf("%s%d", ColIndexToLetters(c), r+1)
endcol := c + cell.HMerge
endrow := r + cell.VMerge + 1
end := fmt.Sprintf("%s%d", ColIndexToLetters(endcol), endrow)
mc.Ref = start + ":" + end
if worksheet.MergeCells == nil {
worksheet.MergeCells = &xlsxMergeCells{}
}
worksheet.MergeCells.Cells = append(worksheet.MergeCells.Cells, mc)
worksheet.MergeCells.addCell(mc)
}
return nil
}
err := row.ForEachCell(makeC, SkipEmptyCells)
if err != nil {
return err
}
xSheet.Row = append(xSheet.Row, xRow)
return nil
}
err := s.ForEachRow(makeR, SkipEmptyRows)
if err != nil {
return err
}
// Update sheet format with the freshly determined max levels
s.SheetFormat.OutlineLevelCol = maxLevelCol
s.SheetFormat.OutlineLevelRow = maxLevelRow
// .. and then also apply this to the xml worksheet
worksheet.SheetFormatPr.OutlineLevelCol = s.SheetFormat.OutlineLevelCol
worksheet.SheetFormatPr.OutlineLevelRow = s.SheetFormat.OutlineLevelRow
if worksheet.MergeCells != nil {
worksheet.MergeCells.Count = len(worksheet.MergeCells.Cells)
}
if s.AutoFilter != nil {
worksheet.AutoFilter = &xlsxAutoFilter{Ref: fmt.Sprintf("%v:%v", s.AutoFilter.TopLeftCell, s.AutoFilter.BottomRightCell)}
}
worksheet.SheetData = xSheet
dimension := xlsxDimension{}
dimension.Ref = "A1:" + GetCellIDStringFromCoords(maxCell, maxRow)
if dimension.Ref == "A1:A1" {
dimension.Ref = "A1"
}
worksheet.Dimension = dimension
return nil
}
func (s *Sheet) makeDataValidations(worksheet *xlsxWorksheet) {
s.mustBeOpen()
if len(s.DataValidations) > 0 {
if worksheet.DataValidations == nil {
worksheet.DataValidations = &xlsxDataValidations{}
}
worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation, s.DataValidations...)
worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation)
}
}
func (s *Sheet) MarshalSheet(w io.Writer, refTable *RefTable, styles *xlsxStyleSheet, relations *xlsxWorksheetRels) error {
worksheet := newXlsxWorksheet()
s.handleMerged()
s.makeSheetView(worksheet)
s.makeSheetFormatPr(worksheet)
maxLevelCol := s.makeCols(worksheet, styles)
s.makeDataValidations(worksheet)
s.prepSheetForMarshalling(maxLevelCol)
err := s.prepWorksheetFromRows(worksheet, relations)
if err != nil {
return err
}
return nil
}
// Dump sheet to its XML representation, intended for internal use only
func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet, relations *xlsxWorksheetRels) *xlsxWorksheet {
s.mustBeOpen()
worksheet := newXlsxWorksheet()
// Scan through the sheet and see if there are any merged cells. If there
// are, we may need to extend the size of the sheet. There needs to be
// phantom cells underlying the area covered by the merged cell
s.handleMerged()
s.makeSheetView(worksheet)
s.makeSheetFormatPr(worksheet)
maxLevelCol := s.makeCols(worksheet, styles)
s.makeDataValidations(worksheet)
s.makeRows(worksheet, styles, refTable, relations, maxLevelCol)
return worksheet
}
func handleStyleForXLSX(style *Style, NumFmtId int, styles *xlsxStyleSheet) (XfId int) {
xFont, xFill, xBorder, xCellXf := style.makeXLSXStyleElements()
fontId := styles.addFont(xFont)
fillId := styles.addFill(xFill)
// HACK - adding light grey fill, as in OO and Google
greyfill := xlsxFill{}
greyfill.PatternFill.PatternType = "lightGray"
styles.addFill(greyfill)
borderId := styles.addBorder(xBorder)
xCellXf.FontId = fontId
xCellXf.FillId = fillId
xCellXf.BorderId = borderId
xCellXf.NumFmtId = NumFmtId
// apply the numFmtId when it is not the default cellxf
if xCellXf.NumFmtId > 0 {
xCellXf.ApplyNumberFormat = true
}
xCellXf.Alignment.Horizontal = style.Alignment.Horizontal
xCellXf.Alignment.Indent = style.Alignment.Indent
xCellXf.Alignment.ShrinkToFit = style.Alignment.ShrinkToFit
xCellXf.Alignment.TextRotation = style.Alignment.TextRotation
xCellXf.Alignment.Vertical = style.Alignment.Vertical
xCellXf.Alignment.WrapText = style.Alignment.WrapText
XfId = styles.addCellXf(xCellXf)
return
}
func handleNumFmtIdForXLSX(NumFmtId int, styles *xlsxStyleSheet) (XfId int) {
xCellXf := makeXLSXCellElement()
xCellXf.NumFmtId = NumFmtId
if xCellXf.NumFmtId > 0 {
xCellXf.ApplyNumberFormat = true
}
XfId = styles.addCellXf(xCellXf)
return
}
func IsSaneSheetName(sheetName string) error {
runeLength := utf8.RuneCountInString(sheetName)
if runeLength > 31 || runeLength == 0 {
return fmt.Errorf("sheet name must be 31 or fewer characters long. It is currently '%d' characters long", runeLength)
}
// Iterate over the runes
for _, r := range sheetName {
// Excel forbids : \ / ? * [ ]
if r == ':' || r == '\\' || r == '/' || r == '?' || r == '*' || r == '[' || r == ']' {
return fmt.Errorf("sheet name must not contain any restricted characters : \\ / ? * [ ] but contains '%s'", string(r))
}
}
return nil
}