This closes #1590, add the Japanese calendar number format support
- The `GetFormControl` now support to get text, rich-text and font format of the form controls - Update the unit tests and the documentation
This commit is contained in:
parent
a07c8cd0b4
commit
5fe30eb456
15
cell.go
15
cell.go
|
@ -1368,15 +1368,24 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er
|
||||||
if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok {
|
if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok {
|
||||||
return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err
|
return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err
|
||||||
}
|
}
|
||||||
|
return f.applyNumFmt(c, styleSheet, numFmtID, date1904, cellType), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyNumFmt provides a function to returns formatted cell value with custom
|
||||||
|
// number format code.
|
||||||
|
func (f *File) applyNumFmt(c *xlsxC, styleSheet *xlsxStyleSheet, numFmtID int, date1904 bool, cellType CellType) string {
|
||||||
if styleSheet.NumFmts == nil {
|
if styleSheet.NumFmts == nil {
|
||||||
return c.V, err
|
return c.V
|
||||||
}
|
}
|
||||||
for _, xlsxFmt := range styleSheet.NumFmts.NumFmt {
|
for _, xlsxFmt := range styleSheet.NumFmts.NumFmt {
|
||||||
if xlsxFmt.NumFmtID == numFmtID {
|
if xlsxFmt.NumFmtID == numFmtID {
|
||||||
return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options), err
|
if xlsxFmt.FormatCode16 != "" {
|
||||||
|
return format(c.V, xlsxFmt.FormatCode16, date1904, cellType, f.options)
|
||||||
|
}
|
||||||
|
return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c.V, err
|
return c.V
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareCellStyle provides a function to prepare style index of cell in
|
// prepareCellStyle provides a function to prepare style index of cell in
|
||||||
|
|
|
@ -927,6 +927,14 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) {
|
||||||
assert.Equal(t, "43528", result)
|
assert.Equal(t, "43528", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyNumFmt(t *testing.T) {
|
||||||
|
f := NewFile()
|
||||||
|
assert.Equal(t, "\u4EE4\u548C\u5143年9月1日", f.applyNumFmt(&xlsxC{V: "43709"},
|
||||||
|
&xlsxStyleSheet{NumFmts: &xlsxNumFmts{NumFmt: []*xlsxNumFmt{
|
||||||
|
{NumFmtID: 164, FormatCode16: "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@"},
|
||||||
|
}}}, 164, false, CellTypeNumber))
|
||||||
|
}
|
||||||
|
|
||||||
func TestSharedStringsError(t *testing.T) {
|
func TestSharedStringsError(t *testing.T) {
|
||||||
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
|
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -7,7 +7,7 @@ require (
|
||||||
github.com/richardlehane/mscfb v1.0.4
|
github.com/richardlehane/mscfb v1.0.4
|
||||||
github.com/stretchr/testify v1.8.0
|
github.com/stretchr/testify v1.8.0
|
||||||
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9
|
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9
|
||||||
github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641
|
github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4
|
||||||
golang.org/x/crypto v0.11.0
|
golang.org/x/crypto v0.11.0
|
||||||
golang.org/x/image v0.5.0
|
golang.org/x/image v0.5.0
|
||||||
golang.org/x/net v0.12.0
|
golang.org/x/net v0.12.0
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -17,8 +17,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E=
|
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E=
|
||||||
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641 h1:1SQuQwUorWlROdGAbsAJrMInj02yCUsYFNi/MzTJ6cA=
|
github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4 h1:7TXNzvlvE0E/oLDazWm2Xip72G9Su+jRzvziSxwO6Ww=
|
||||||
github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
|
43
numfmt.go
43
numfmt.go
|
@ -26,6 +26,7 @@ import (
|
||||||
type languageInfo struct {
|
type languageInfo struct {
|
||||||
apFmt string
|
apFmt string
|
||||||
tags []string
|
tags []string
|
||||||
|
useGannen bool
|
||||||
localMonth func(t time.Time, abbr int) string
|
localMonth func(t time.Time, abbr int) string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -795,6 +796,8 @@ var (
|
||||||
"10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]},
|
"10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]},
|
||||||
"11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese},
|
"11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese},
|
||||||
"411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese},
|
"411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese},
|
||||||
|
"800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese},
|
||||||
|
"JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, useGannen: true},
|
||||||
"12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean},
|
"12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean},
|
||||||
"412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean},
|
"412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean},
|
||||||
"7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]},
|
"7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]},
|
||||||
|
@ -833,6 +836,18 @@ var (
|
||||||
"35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]},
|
"35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]},
|
||||||
"435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]},
|
"435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]},
|
||||||
}
|
}
|
||||||
|
// japaneseEraYears list the Japanese era name periods.
|
||||||
|
japaneseEraYears = []time.Time{
|
||||||
|
time.Date(1868, time.August, 8, 0, 0, 0, 0, time.UTC),
|
||||||
|
time.Date(1912, time.June, 30, 0, 0, 0, 0, time.UTC),
|
||||||
|
time.Date(1926, time.November, 25, 0, 0, 0, 0, time.UTC),
|
||||||
|
time.Date(1989, time.January, 8, 0, 0, 0, 0, time.UTC),
|
||||||
|
time.Date(2019, time.April, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
}
|
||||||
|
// japaneseEraNames list the Japanese era name for the Japanese emperor reign calendar.
|
||||||
|
japaneseEraNames = []string{"\u660E\u6CBB", "\u5927\u6B63", "\u662D\u548C", "\u5E73\u6210", "\u4EE4\u548C"}
|
||||||
|
// japaneseEraYear list the Japanese era name symbols.
|
||||||
|
japaneseEraSymbols = []string{"M", "T", "S", "H", "R"}
|
||||||
// monthNamesBangla list the month names in the Bangla.
|
// monthNamesBangla list the month names in the Bangla.
|
||||||
monthNamesBangla = []string{
|
monthNamesBangla = []string{
|
||||||
"\u099C\u09BE\u09A8\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0",
|
"\u099C\u09BE\u09A8\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0",
|
||||||
|
@ -1329,8 +1344,10 @@ func (nf *numberFormat) dateTimeHandler() string {
|
||||||
if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode {
|
if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode {
|
||||||
return nf.value
|
return nf.value
|
||||||
}
|
}
|
||||||
|
if !supportedLanguageInfo[nf.localCode].useGannen {
|
||||||
nf.result += nf.currencyString
|
nf.result += nf.currencyString
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if token.TType == nfp.TokenTypeDateTimes {
|
if token.TType == nfp.TokenTypeDateTimes {
|
||||||
nf.dateTimesHandler(i, token)
|
nf.dateTimesHandler(i, token)
|
||||||
}
|
}
|
||||||
|
@ -1752,15 +1769,35 @@ func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) {
|
||||||
// yearsHandler will be handling years in the date and times types tokens for a
|
// yearsHandler will be handling years in the date and times types tokens for a
|
||||||
// number format expression.
|
// number format expression.
|
||||||
func (nf *numberFormat) yearsHandler(token nfp.Token) {
|
func (nf *numberFormat) yearsHandler(token nfp.Token) {
|
||||||
years := strings.Contains(strings.ToUpper(token.TValue), "Y")
|
if strings.Contains(strings.ToUpper(token.TValue), "Y") {
|
||||||
if years && len(token.TValue) <= 2 {
|
if len(token.TValue) <= 2 {
|
||||||
nf.result += strconv.Itoa(nf.t.Year())[2:]
|
nf.result += strconv.Itoa(nf.t.Year())[2:]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if years && len(token.TValue) > 2 {
|
|
||||||
nf.result += strconv.Itoa(nf.t.Year())
|
nf.result += strconv.Itoa(nf.t.Year())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if strings.Contains(strings.ToUpper(token.TValue), "G") {
|
||||||
|
for i := len(japaneseEraYears) - 1; i > 0; i-- {
|
||||||
|
if y := japaneseEraYears[i]; nf.t.After(y) {
|
||||||
|
switch len(token.TValue) {
|
||||||
|
case 1:
|
||||||
|
nf.result += japaneseEraSymbols[i]
|
||||||
|
case 2:
|
||||||
|
nf.result += japaneseEraNames[i][:3]
|
||||||
|
default:
|
||||||
|
nf.result += japaneseEraNames[i]
|
||||||
|
}
|
||||||
|
year := nf.t.Year() - y.Year() + 1
|
||||||
|
if year == 1 && len(token.TValue) > 1 && supportedLanguageInfo[nf.localCode].useGannen {
|
||||||
|
nf.result += "\u5143"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nf.result += strconv.Itoa(year)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// daysHandler will be handling days in the date and times types tokens for a
|
// daysHandler will be handling days in the date and times types tokens for a
|
||||||
|
|
|
@ -420,6 +420,18 @@ func TestNumFmt(t *testing.T) {
|
||||||
{"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"},
|
{"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"},
|
||||||
{"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"},
|
{"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"},
|
||||||
{"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"},
|
{"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"},
|
||||||
|
{"43709", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"},
|
||||||
|
{"43709", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"},
|
||||||
|
{"43709", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C1年9月1日"},
|
||||||
|
{"43709", "[$-ja-JP-x-gannen,80]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"},
|
||||||
|
{"43709", "[$-ja-JP-x-gannen,80]gge\"年\"m\"月\"d\"日\";@", "\u4EE4\u5143年9月1日"},
|
||||||
|
{"43709", "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C\u5143年9月1日"},
|
||||||
|
{"43466.189571759256", "[$-411]ge\"年\"m\"月\"d\"日\";@", "H31年1月1日"},
|
||||||
|
{"43466.189571759256", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u5E7331年1月1日"},
|
||||||
|
{"43466.189571759256", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u5E73\u621031年1月1日"},
|
||||||
|
{"44896.18957170139", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R4年12月1日"},
|
||||||
|
{"44896.18957170139", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE44年12月1日"},
|
||||||
|
{"44896.18957170139", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C4年12月1日"},
|
||||||
{"44562.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
|
{"44562.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
|
||||||
{"44593.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
|
{"44593.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
|
||||||
{"44621.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
|
{"44621.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
|
||||||
|
|
110
vml.go
110
vml.go
|
@ -420,11 +420,17 @@ func (f *File) DeleteFormControl(sheet, cell string) error {
|
||||||
for i, sp := range vml.Shape {
|
for i, sp := range vml.Shape {
|
||||||
var shapeVal decodeShapeVal
|
var shapeVal decodeShapeVal
|
||||||
if err = xml.Unmarshal([]byte(fmt.Sprintf("<shape>%s</shape>", sp.Val)), &shapeVal); err == nil &&
|
if err = xml.Unmarshal([]byte(fmt.Sprintf("<shape>%s</shape>", sp.Val)), &shapeVal); err == nil &&
|
||||||
shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Column == col-1 && shapeVal.ClientData.Row == row-1 {
|
shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Anchor != "" {
|
||||||
|
leftCol, topRow, err := extractAnchorCell(shapeVal.ClientData.Anchor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if leftCol == col-1 && topRow == row-1 {
|
||||||
vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...)
|
vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
f.VMLDrawing[drawingVML] = vml
|
f.VMLDrawing[drawingVML] = vml
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -454,7 +460,7 @@ func (f *File) decodeVMLDrawingReader(path string) (*decodeVmlDrawing, error) {
|
||||||
c, ok := f.Pkg.Load(path)
|
c, ok := f.Pkg.Load(path)
|
||||||
if ok && c != nil {
|
if ok && c != nil {
|
||||||
f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
|
f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
|
||||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))).
|
if err := f.xmlNewDecoder(bytes.NewReader(bytesReplace(namespaceStrictToTransitional(c.([]byte)), []byte("<br>\r\n"), []byte("<br></br>\r\n"), -1))).
|
||||||
Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
|
Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -574,6 +580,9 @@ func formCtrlText(opts *vmlOptions) []vmlFont {
|
||||||
if run.Font.Underline == "single" {
|
if run.Font.Underline == "single" {
|
||||||
fnt.Content = "<u>" + fnt.Content + "</u>"
|
fnt.Content = "<u>" + fnt.Content + "</u>"
|
||||||
}
|
}
|
||||||
|
if run.Font.Underline == "double" {
|
||||||
|
fnt.Content = "<u class=\"font1\">" + fnt.Content + "</u>"
|
||||||
|
}
|
||||||
if run.Font.Italic {
|
if run.Font.Italic {
|
||||||
fnt.Content = "<i>" + fnt.Content + "</i>"
|
fnt.Content = "<i>" + fnt.Content + "</i>"
|
||||||
}
|
}
|
||||||
|
@ -765,8 +774,8 @@ func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor stri
|
||||||
ObjectType: preset.objectType,
|
ObjectType: preset.objectType,
|
||||||
Anchor: anchor,
|
Anchor: anchor,
|
||||||
AutoFill: preset.autoFill,
|
AutoFill: preset.autoFill,
|
||||||
Row: row - 1,
|
Row: intPtr(row - 1),
|
||||||
Column: col - 1,
|
Column: intPtr(col - 1),
|
||||||
TextHAlign: preset.textHAlign,
|
TextHAlign: preset.textHAlign,
|
||||||
TextVAlign: preset.textVAlign,
|
TextVAlign: preset.textVAlign,
|
||||||
NoThreeD: preset.noThreeD,
|
NoThreeD: preset.noThreeD,
|
||||||
|
@ -885,8 +894,8 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFormControls retrieves all form controls in a worksheet by a given
|
// GetFormControls retrieves all form controls in a worksheet by a given
|
||||||
// worksheet name. Note that, this function does not support getting the width,
|
// worksheet name. Note that, this function does not support getting the width
|
||||||
// height, text, rich text, and format currently.
|
// and height of the form controls currently.
|
||||||
func (f *File) GetFormControls(sheet string) ([]FormControl, error) {
|
func (f *File) GetFormControls(sheet string) ([]FormControl, error) {
|
||||||
var formControls []FormControl
|
var formControls []FormControl
|
||||||
// Read sheet data
|
// Read sheet data
|
||||||
|
@ -949,9 +958,18 @@ func extractFormControl(clientData string) (FormControl, error) {
|
||||||
return formControl, err
|
return formControl, err
|
||||||
}
|
}
|
||||||
for formCtrlType, preset := range formCtrlPresets {
|
for formCtrlType, preset := range formCtrlPresets {
|
||||||
if shapeVal.ClientData.ObjectType == preset.objectType {
|
if shapeVal.ClientData.ObjectType == preset.objectType && shapeVal.ClientData.Anchor != "" {
|
||||||
|
formControl.Paragraph = extractVMLFont(shapeVal.TextBox.Div.Font)
|
||||||
|
if len(formControl.Paragraph) > 0 && formControl.Paragraph[0].Font == nil {
|
||||||
|
formControl.Text = formControl.Paragraph[0].Text
|
||||||
|
formControl.Paragraph = formControl.Paragraph[1:]
|
||||||
|
}
|
||||||
formControl.Type = formCtrlType
|
formControl.Type = formCtrlType
|
||||||
if formControl.Cell, err = CoordinatesToCellName(shapeVal.ClientData.Column+1, shapeVal.ClientData.Row+1); err != nil {
|
col, row, err := extractAnchorCell(shapeVal.ClientData.Anchor)
|
||||||
|
if err != nil {
|
||||||
|
return formControl, err
|
||||||
|
}
|
||||||
|
if formControl.Cell, err = CoordinatesToCellName(col+1, row+1); err != nil {
|
||||||
return formControl, err
|
return formControl, err
|
||||||
}
|
}
|
||||||
formControl.Macro = shapeVal.ClientData.FmlaMacro
|
formControl.Macro = shapeVal.ClientData.FmlaMacro
|
||||||
|
@ -967,3 +985,79 @@ func extractFormControl(clientData string) (FormControl, error) {
|
||||||
}
|
}
|
||||||
return formControl, err
|
return formControl, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractAnchorCell extract left-top cell coordinates from given VML anchor
|
||||||
|
// comma-separated list values.
|
||||||
|
func extractAnchorCell(anchor string) (int, int, error) {
|
||||||
|
var (
|
||||||
|
leftCol, topRow int
|
||||||
|
err error
|
||||||
|
pos = strings.Split(anchor, ",")
|
||||||
|
)
|
||||||
|
if len(pos) != 8 {
|
||||||
|
return leftCol, topRow, ErrParameterInvalid
|
||||||
|
}
|
||||||
|
leftCol, err = strconv.Atoi(strings.TrimSpace(pos[0]))
|
||||||
|
if err != nil {
|
||||||
|
return leftCol, topRow, ErrColumnNumber
|
||||||
|
}
|
||||||
|
topRow, err = strconv.Atoi(strings.TrimSpace(pos[2]))
|
||||||
|
return leftCol, topRow, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractVMLFont extract rich-text and font format from given VML font element.
|
||||||
|
func extractVMLFont(font []decodeVMLFont) []RichTextRun {
|
||||||
|
var runs []RichTextRun
|
||||||
|
extractU := func(u *decodeVMLFontU, run *RichTextRun) {
|
||||||
|
if u == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
run.Text += u.Val
|
||||||
|
if run.Font == nil {
|
||||||
|
run.Font = &Font{}
|
||||||
|
}
|
||||||
|
run.Font.Underline = "single"
|
||||||
|
if u.Class == "font1" {
|
||||||
|
run.Font.Underline = "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extractI := func(i *decodeVMLFontI, run *RichTextRun) {
|
||||||
|
if i == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
extractU(i.U, run)
|
||||||
|
run.Text += i.Val
|
||||||
|
if run.Font == nil {
|
||||||
|
run.Font = &Font{}
|
||||||
|
}
|
||||||
|
run.Font.Italic = true
|
||||||
|
}
|
||||||
|
extractB := func(b *decodeVMLFontB, run *RichTextRun) {
|
||||||
|
if b == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
extractI(b.I, run)
|
||||||
|
run.Text += b.Val
|
||||||
|
if run.Font == nil {
|
||||||
|
run.Font = &Font{}
|
||||||
|
}
|
||||||
|
run.Font.Bold = true
|
||||||
|
}
|
||||||
|
for _, fnt := range font {
|
||||||
|
var run RichTextRun
|
||||||
|
extractB(fnt.B, &run)
|
||||||
|
extractI(fnt.I, &run)
|
||||||
|
extractU(fnt.U, &run)
|
||||||
|
run.Text += fnt.Val
|
||||||
|
if fnt.Face != "" || fnt.Size > 0 || fnt.Color != "" {
|
||||||
|
if run.Font == nil {
|
||||||
|
run.Font = &Font{}
|
||||||
|
}
|
||||||
|
run.Font.Family = fnt.Face
|
||||||
|
run.Font.Size = float64(fnt.Size / 20)
|
||||||
|
run.Font.Color = fnt.Color
|
||||||
|
}
|
||||||
|
runs = append(runs, run)
|
||||||
|
}
|
||||||
|
return runs
|
||||||
|
}
|
||||||
|
|
|
@ -137,8 +137,8 @@ type xClientData struct {
|
||||||
FmlaMacro string `xml:"x:FmlaMacro,omitempty"`
|
FmlaMacro string `xml:"x:FmlaMacro,omitempty"`
|
||||||
TextHAlign string `xml:"x:TextHAlign,omitempty"`
|
TextHAlign string `xml:"x:TextHAlign,omitempty"`
|
||||||
TextVAlign string `xml:"x:TextVAlign,omitempty"`
|
TextVAlign string `xml:"x:TextVAlign,omitempty"`
|
||||||
Row int `xml:"x:Row"`
|
Row *int `xml:"x:Row"`
|
||||||
Column int `xml:"x:Column"`
|
Column *int `xml:"x:Column"`
|
||||||
Checked int `xml:"x:Checked,omitempty"`
|
Checked int `xml:"x:Checked,omitempty"`
|
||||||
FmlaLink string `xml:"x:FmlaLink,omitempty"`
|
FmlaLink string `xml:"x:FmlaLink,omitempty"`
|
||||||
NoThreeD *string `xml:"x:NoThreeD"`
|
NoThreeD *string `xml:"x:NoThreeD"`
|
||||||
|
@ -185,16 +185,59 @@ type decodeShape struct {
|
||||||
// decodeShapeVal defines the structure used to parse the sub-element of the
|
// decodeShapeVal defines the structure used to parse the sub-element of the
|
||||||
// shape in the file xl/drawings/vmlDrawing%d.vml.
|
// shape in the file xl/drawings/vmlDrawing%d.vml.
|
||||||
type decodeShapeVal struct {
|
type decodeShapeVal struct {
|
||||||
|
TextBox decodeVMLTextBox `xml:"textbox"`
|
||||||
ClientData decodeVMLClientData `xml:"ClientData"`
|
ClientData decodeVMLClientData `xml:"ClientData"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeVMLFontU defines the structure used to parse the u element in the VML.
|
||||||
|
type decodeVMLFontU struct {
|
||||||
|
Class string `xml:"class,attr"`
|
||||||
|
Val string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeVMLFontI defines the structure used to parse the i element in the VML.
|
||||||
|
type decodeVMLFontI struct {
|
||||||
|
U *decodeVMLFontU `xml:"u"`
|
||||||
|
Val string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeVMLFontB defines the structure used to parse the b element in the VML.
|
||||||
|
type decodeVMLFontB struct {
|
||||||
|
I *decodeVMLFontI `xml:"i"`
|
||||||
|
U *decodeVMLFontU `xml:"u"`
|
||||||
|
Val string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeVMLFont defines the structure used to parse the font element in the VML.
|
||||||
|
type decodeVMLFont struct {
|
||||||
|
Face string `xml:"face,attr,omitempty"`
|
||||||
|
Size uint `xml:"size,attr,omitempty"`
|
||||||
|
Color string `xml:"color,attr,omitempty"`
|
||||||
|
B *decodeVMLFontB `xml:"b"`
|
||||||
|
I *decodeVMLFontI `xml:"i"`
|
||||||
|
U *decodeVMLFontU `xml:"u"`
|
||||||
|
Val string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeVMLDiv defines the structure used to parse the div element in the VML.
|
||||||
|
type decodeVMLDiv struct {
|
||||||
|
Font []decodeVMLFont `xml:"font"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeVMLTextBox defines the structure used to parse the v:textbox element in
|
||||||
|
// the file xl/drawings/vmlDrawing%d.vml.
|
||||||
|
type decodeVMLTextBox struct {
|
||||||
|
Div decodeVMLDiv `xml:"div"`
|
||||||
|
}
|
||||||
|
|
||||||
// decodeVMLClientData defines the structure used to parse the x:ClientData
|
// decodeVMLClientData defines the structure used to parse the x:ClientData
|
||||||
// element in the file xl/drawings/vmlDrawing%d.vml.
|
// element in the file xl/drawings/vmlDrawing%d.vml.
|
||||||
type decodeVMLClientData struct {
|
type decodeVMLClientData struct {
|
||||||
ObjectType string `xml:"ObjectType,attr"`
|
ObjectType string `xml:"ObjectType,attr"`
|
||||||
|
Anchor string
|
||||||
FmlaMacro string
|
FmlaMacro string
|
||||||
Column int
|
Column *int
|
||||||
Row int
|
Row *int
|
||||||
Checked int
|
Checked int
|
||||||
FmlaLink string
|
FmlaLink string
|
||||||
Val uint
|
Val uint
|
||||||
|
|
53
vml_test.go
53
vml_test.go
|
@ -164,7 +164,7 @@ func TestFormControl(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
|
Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
|
||||||
Width: 140, Height: 60, Text: "Button 1\r\n",
|
Width: 140, Height: 60, Text: "Button 1\n",
|
||||||
Paragraph: []RichTextRun{
|
Paragraph: []RichTextRun{
|
||||||
{
|
{
|
||||||
Font: &Font{
|
Font: &Font{
|
||||||
|
@ -234,6 +234,8 @@ func TestFormControl(t *testing.T) {
|
||||||
assert.Equal(t, formCtrl.IncChange, result[i].IncChange)
|
assert.Equal(t, formCtrl.IncChange, result[i].IncChange)
|
||||||
assert.Equal(t, formCtrl.Horizontally, result[i].Horizontally)
|
assert.Equal(t, formCtrl.Horizontally, result[i].Horizontally)
|
||||||
assert.Equal(t, formCtrl.CellLink, result[i].CellLink)
|
assert.Equal(t, formCtrl.CellLink, result[i].CellLink)
|
||||||
|
assert.Equal(t, formCtrl.Text, result[i].Text)
|
||||||
|
assert.Equal(t, len(formCtrl.Paragraph), len(result[i].Paragraph))
|
||||||
}
|
}
|
||||||
assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")}))
|
assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")}))
|
||||||
file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin"))
|
file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin"))
|
||||||
|
@ -249,7 +251,8 @@ func TestFormControl(t *testing.T) {
|
||||||
assert.Len(t, result, 11)
|
assert.Len(t, result, 11)
|
||||||
// Test add from control to a worksheet which already contains form controls
|
// Test add from control to a worksheet which already contains form controls
|
||||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||||
Cell: "D4", Type: FormControlButton, Macro: "Button1_Click", Text: "Button 2",
|
Cell: "D4", Type: FormControlButton, Macro: "Button1_Click",
|
||||||
|
Paragraph: []RichTextRun{{Font: &Font{Underline: "double"}, Text: "Button 2"}},
|
||||||
}))
|
}))
|
||||||
// Test get from controls after add form controls
|
// Test get from controls after add form controls
|
||||||
result, err = f.GetFormControls("Sheet1")
|
result, err = f.GetFormControls("Sheet1")
|
||||||
|
@ -297,6 +300,11 @@ func TestFormControl(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
|
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
|
||||||
assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8")
|
assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8")
|
||||||
|
// Test delete form controls with invalid shape anchor
|
||||||
|
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||||
|
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Scroll\"><x:Anchor>0</x:Anchor></x:ClientData>"}},
|
||||||
|
}
|
||||||
|
assert.Equal(t, ErrParameterInvalid, f.DeleteFormControl("Sheet1", "A1"))
|
||||||
assert.NoError(t, f.Close())
|
assert.NoError(t, f.Close())
|
||||||
// Test delete form control on a worksheet without form control
|
// Test delete form control on a worksheet without form control
|
||||||
f = NewFile()
|
f = NewFile()
|
||||||
|
@ -320,9 +328,39 @@ func TestFormControl(t *testing.T) {
|
||||||
formControls, err = f.GetFormControls("Sheet1")
|
formControls, err = f.GetFormControls("Sheet1")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, formControls, 0)
|
assert.Len(t, formControls, 0)
|
||||||
|
// Test get form controls with bold font format
|
||||||
|
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||||
|
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><b>Text</b></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
|
||||||
|
}
|
||||||
|
formControls, err = f.GetFormControls("Sheet1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, formControls[0].Paragraph[0].Font.Bold)
|
||||||
|
// Test get form controls with italic font format
|
||||||
|
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||||
|
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><i>Text</i></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
|
||||||
|
}
|
||||||
|
formControls, err = f.GetFormControls("Sheet1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, formControls[0].Paragraph[0].Font.Italic)
|
||||||
|
// Test get form controls with font format
|
||||||
|
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||||
|
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font face=\"Calibri\" size=\"280\" color=\"#777777\">Text</font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
|
||||||
|
}
|
||||||
|
formControls, err = f.GetFormControls("Sheet1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Calibri", formControls[0].Paragraph[0].Font.Family)
|
||||||
|
assert.Equal(t, 14.0, formControls[0].Paragraph[0].Font.Size)
|
||||||
|
assert.Equal(t, "#777777", formControls[0].Paragraph[0].Font.Color)
|
||||||
|
// Test get form controls with italic font format
|
||||||
|
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||||
|
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><i>Text</i></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
|
||||||
|
}
|
||||||
|
formControls, err = f.GetFormControls("Sheet1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, formControls[0].Paragraph[0].Font.Italic)
|
||||||
// Test get form controls with invalid column number
|
// Test get form controls with invalid column number
|
||||||
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||||
Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Column>%d</x:Column></x:ClientData>", MaxColumns)}},
|
Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Anchor>%d,0,0,0,0,0,0,0</x:Anchor></x:ClientData>", MaxColumns)}},
|
||||||
}
|
}
|
||||||
formControls, err = f.GetFormControls("Sheet1")
|
formControls, err = f.GetFormControls("Sheet1")
|
||||||
assert.Equal(t, err, ErrColumnNumber)
|
assert.Equal(t, err, ErrColumnNumber)
|
||||||
|
@ -343,11 +381,18 @@ func TestFormControl(t *testing.T) {
|
||||||
assert.Len(t, formControls, 0)
|
assert.Len(t, formControls, 0)
|
||||||
// Test get form controls with invalid column number
|
// Test get form controls with invalid column number
|
||||||
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
|
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
|
||||||
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Column>%d</x:Column></x:ClientData>", MaxColumns)}},
|
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Anchor>%d,0,0,0,0,0,0,0</x:Anchor></x:ClientData>", MaxColumns)}},
|
||||||
}
|
}
|
||||||
formControls, err = f.GetFormControls("Sheet1")
|
formControls, err = f.GetFormControls("Sheet1")
|
||||||
assert.Equal(t, err, ErrColumnNumber)
|
assert.Equal(t, err, ErrColumnNumber)
|
||||||
assert.Len(t, formControls, 0)
|
assert.Len(t, formControls, 0)
|
||||||
|
// Test get form controls with invalid shape anchor
|
||||||
|
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
|
||||||
|
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Scroll\"><x:Anchor>x,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
|
||||||
|
}
|
||||||
|
formControls, err = f.GetFormControls("Sheet1")
|
||||||
|
assert.Equal(t, ErrColumnNumber, err)
|
||||||
|
assert.Len(t, formControls, 0)
|
||||||
// Test get form controls with comment (Note) shape type
|
// Test get form controls with comment (Note) shape type
|
||||||
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
|
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
|
||||||
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Note\"></x:ClientData>"}},
|
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Note\"></x:ClientData>"}},
|
||||||
|
|
|
@ -302,6 +302,7 @@ type xlsxNumFmts struct {
|
||||||
type xlsxNumFmt struct {
|
type xlsxNumFmt struct {
|
||||||
NumFmtID int `xml:"numFmtId,attr"`
|
NumFmtID int `xml:"numFmtId,attr"`
|
||||||
FormatCode string `xml:"formatCode,attr,omitempty"`
|
FormatCode string `xml:"formatCode,attr,omitempty"`
|
||||||
|
FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// xlsxStyleColors directly maps the colors' element. Color information
|
// xlsxStyleColors directly maps the colors' element. Color information
|
||||||
|
|
Loading…
Reference in New Issue