1 Star 0 Fork 0

where / go-xlsx-templater

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
xlst.go 10.35 KB
一键复制 编辑 原始数据 按行查看 历史
lucky51 提交于 2022-09-07 08:34 . 添加时间段合并行
package xlst
import (
"errors"
"fmt"
"io"
"reflect"
"regexp"
"strings"
//"math/rand"
"time"
"github.com/aymerick/raymond"
//"github.com/tealeg/xlsx"
// 为了支持执行计划的报表生成做了适当的修改
"gitee.com/lucky51/xlsx"
)
var (
rgx = regexp.MustCompile(`\{\{\s*(\w+)\.\w+\s*\}\}`)
rangeRgx = regexp.MustCompile(`\{\{\s*range\s+(\w+)\s*\}\}`)
rangeEndRgx = regexp.MustCompile(`\{\{\s*end\s*\}\}`)
rgxLine = regexp.MustCompile(`\<\<\s*line\s*\>\>`)
boldValue = regexp.MustCompile(`\_\_([\s\S]+)\_\_`)
timeRgx = regexp.MustCompile(`^\d{2}:\d{2}$`)
TimeLayout = "15:04"
)
// Xlst Represents template struct
type Xlst struct {
file *xlsx.File
report *xlsx.File
}
// Options for render has only one property WrapTextInAllCells for wrapping text
type Options struct {
WrapTextInAllCells bool
}
// New creates new Xlst struct and returns pointer to it
func New() *Xlst {
return &Xlst{}
}
// InParam 输入参数
type InParam struct {
In interface{}
SheetName string
}
// NewFromBinary creates new Xlst struct puts binary tempate into and returns pointer to it
func NewFromBinary(content []byte) (*Xlst, error) {
file, err := xlsx.OpenBinary(content)
if err != nil {
return nil, err
}
res := &Xlst{file: file}
return res, nil
}
// Render renders report and stores it in a struct
func (m *Xlst) Render(in interface{}) error {
return m.RenderWithOptions(in, nil)
}
type TimeSpan struct {
Start time.Time
End time.Time
Text string
}
func (t *TimeSpan) Has(timeStr string) bool {
tVal, err := time.ParseInLocation("15:04", timeStr, time.Local)
if err != nil {
return false
}
if (tVal.Equal(t.Start) || tVal.Equal(t.End)) || (tVal.After(t.Start) && tVal.Before(t.End)) {
return true
}
return false
}
// MergeTimeSpan 根据时间合并行
func (m *Xlst) MergeTimeSpan(timeSpans []*TimeSpan) {
for _, s := range m.report.Sheets {
memo := map[*TimeSpan][]int{}
for j := range s.Rows {
c := s.Cell(j, 0)
if timeRgx.MatchString(c.Value) {
for _, t := range timeSpans {
if t.Has(c.Value) {
memo[t] = append(memo[t], j)
break
}
}
}
}
for k, values := range memo {
if len(values) == 0 {
continue
}
cnt := 0
for i := 0; i < len(values); {
p := i
for ; p+1 < len(values) && values[p]+1 == values[p+1]; p++ {
cnt++
}
c := s.Cell(values[i], 0)
if cnt > 0 {
c.Merge(0, cnt)
}
c.SetValue(k.Text)
cnt = 0
if i == p {
i++
} else {
i = p
}
}
}
}
}
// RenderMultipleSheet renders multipleSheet
func (m *Xlst) RenderMultipleSheet(options *Options, in ...InParam) error {
if options == nil {
options = new(Options)
}
report := xlsx.NewFile()
//TODO: 模板中多个sheet会出问题
for si, sheet := range m.file.Sheets {
for idx, item := range in {
ctx := getCtx(item.In, si)
if item.SheetName == "" {
_, e1 := report.AddSheet(fmt.Sprintf("Sheet%d", idx))
if e1 != nil {
fmt.Errorf("%+v", e1)
}
} else {
_, e := report.AddSheet(fmt.Sprintf("%d.%s", idx+1, item.SheetName))
if e != nil {
fmt.Errorf("%+v", e)
}
}
cloneSheet(sheet, report.Sheets[idx])
//删除最后一个 page break
report.Sheets[idx].RemoveTheLastPageBreak()
err := renderRows(report.Sheets[idx], sheet.Rows, ctx, options)
if err != nil {
return err
}
for _, col := range sheet.Cols {
report.Sheets[idx].Cols = append(report.Sheets[idx].Cols, col)
}
}
}
m.report = report
return nil
}
// RenderWithOptions renders report with options provided and stores it in a struct
func (m *Xlst) RenderWithOptions(in interface{}, options *Options) error {
if options == nil {
options = new(Options)
}
report := xlsx.NewFile()
for si, sheet := range m.file.Sheets {
ctx := getCtx(in, si)
report.AddSheet(sheet.Name)
cloneSheet(sheet, report.Sheets[si])
err := renderRows(report.Sheets[si], sheet.Rows, ctx, options)
if err != nil {
return err
}
for _, col := range sheet.Cols {
report.Sheets[si].Cols = append(report.Sheets[si].Cols, col)
}
}
m.report = report
return nil
}
// ReadTemplate reads template from disk and stores it in a struct
func (m *Xlst) ReadTemplate(path string) error {
file, err := xlsx.OpenFile(path)
if err != nil {
return err
}
m.file = file
return nil
}
// Save saves generated report to disk
func (m *Xlst) Save(path string) error {
if m.report == nil {
return errors.New("Report was not generated")
}
return m.report.Save(path)
}
// Write writes generated report to provided writer
func (m *Xlst) Write(writer io.Writer) error {
if m.report == nil {
return errors.New("Report was not generated")
}
return m.report.Write(writer)
}
func renderRows(sheet *xlsx.Sheet, rows []*xlsx.Row, ctx map[string]interface{}, options *Options) error {
for ri := 0; ri < len(rows); ri++ {
row := rows[ri]
rangeProp := getRangeProp(row)
if rangeProp != "" {
ri++
rangeEndIndex := getRangeEndIndex(rows[ri:])
if rangeEndIndex == -1 {
return fmt.Errorf("End of range %q not found", rangeProp)
}
rangeEndIndex += ri
rangeCtx := getRangeCtx(ctx, rangeProp)
if rangeCtx == nil {
return fmt.Errorf("Not expected context property for range %q", rangeProp)
}
for idx := range rangeCtx {
localCtx := mergeCtx(rangeCtx[idx], ctx)
err := renderRows(sheet, rows[ri:rangeEndIndex], localCtx, options)
if err != nil {
return err
}
}
ri = rangeEndIndex
continue
}
prop, isLine := getListProp(row)
//修改支持分隔符
if isLine {
sheet.InsertPageBreak(fmt.Sprintf("A%d", sheet.MaxRow+1))
continue
}
if prop == "" {
newRow := sheet.AddRow()
cloneRow(row, newRow, options)
err := renderRow(newRow, ctx)
if err != nil {
return err
}
continue
}
if !isArray(ctx, prop) {
newRow := sheet.AddRow()
cloneRow(row, newRow, options)
err := renderRow(newRow, ctx)
if err != nil {
return err
}
continue
}
arr := reflect.ValueOf(ctx[prop])
arrBackup := ctx[prop]
if arr.Len() == 0 {
s := xlsx.NewStyle()
s.Alignment.Vertical = "center"
s.Alignment.Horizontal = "center"
s.Font.Italic = true
s.Font.Size = 8
s.Border = *xlsx.NewBorder("thin", "thin", "thin", "thin")
s.ApplyBorder = true
s.ApplyFont = true
s.ApplyAlignment = true
newRow := sheet.AddRow()
newRow.SetHeight(20)
first := newRow.AddCell()
// issue:merge cell style
for i := 0; i < 8; i++ {
newRow.AddCell().SetStyle(s)
}
first.Merge(8, 0)
first.Value = "没有记录"
first.SetStyle(s)
continue
}
for i := 0; i < arr.Len(); i++ {
newRow := sheet.AddRow()
cloneRow(row, newRow, options)
ctx[prop] = arr.Index(i).Interface()
err := renderRow(newRow, ctx)
if err != nil {
return err
}
}
ctx[prop] = arrBackup
}
return nil
}
func cloneCell(from, to *xlsx.Cell, options *Options) {
to.Value = from.Value
style := from.GetStyle()
if options.WrapTextInAllCells {
style.Alignment.WrapText = true
}
to.SetStyle(style)
to.HMerge = from.HMerge
to.VMerge = from.VMerge
to.Hidden = from.Hidden
to.NumFmt = from.NumFmt
}
func cloneRow(from, to *xlsx.Row, options *Options) {
if from.Height != 0 {
// 为了支持文字换行高度自适应
//to.SetHeight(from.Height)
}
for _, cell := range from.Cells {
newCell := to.AddCell()
cloneCell(cell, newCell, options)
}
}
func renderCell(cell *xlsx.Cell, ctx interface{}) error {
tpl := strings.Replace(cell.Value, "{{", "{{{", -1)
tpl = strings.Replace(tpl, "}}", "}}}", -1)
template, err := raymond.Parse(tpl)
if err != nil {
return err
}
out, err := template.Exec(ctx)
if err != nil {
return err
}
cell.Value = out
if match := boldValue.FindAllStringSubmatch(cell.Value, -1); match != nil {
//cloneSheet
cell.Value = match[0][1]
newStyle := *(cell.GetStyle())
newStyle.Font.Color = xlsx.RGB_White
newStyle.Fill.FgColor = "FFEEEEEE"
newStyle.Fill.PatternType = "solid"
//newStyle.Font.Bold = true
newStyle.ApplyFill = true
cell.SetStyle(&newStyle)
}
return nil
}
func cloneSheet(from, to *xlsx.Sheet) {
to.ColBreaks = from.ColBreaks
to.RowBreaks = from.RowBreaks
for _, col := range from.Cols {
newCol := xlsx.Col{}
style := col.GetStyle()
newCol.SetStyle(style)
newCol.Width = col.Width
newCol.Hidden = col.Hidden
newCol.Collapsed = col.Collapsed
newCol.Min = col.Min
newCol.Max = col.Max
to.Cols = append(to.Cols, &newCol)
}
}
func getCtx(in interface{}, i int) map[string]interface{} {
if ctx, ok := in.(map[string]interface{}); ok {
return ctx
}
if ctxSlice, ok := in.([]interface{}); ok {
if len(ctxSlice) > i {
_ctx := ctxSlice[i]
if ctx, ok := _ctx.(map[string]interface{}); ok {
return ctx
}
}
return nil
}
return nil
}
func getRangeCtx(ctx map[string]interface{}, prop string) []map[string]interface{} {
val, ok := ctx[prop]
if !ok {
return nil
}
if propCtx, ok := val.([]map[string]interface{}); ok {
return propCtx
}
return nil
}
func mergeCtx(local, global map[string]interface{}) map[string]interface{} {
ctx := make(map[string]interface{})
for k, v := range global {
ctx[k] = v
}
for k, v := range local {
ctx[k] = v
}
return ctx
}
func isArray(in map[string]interface{}, prop string) bool {
val, ok := in[prop]
if !ok {
return false
}
switch reflect.TypeOf(val).Kind() {
case reflect.Array, reflect.Slice:
return true
}
return false
}
func getListProp(in *xlsx.Row) (string, bool) {
for _, cell := range in.Cells {
if cell.Value == "" {
continue
}
if match := rgx.FindAllStringSubmatch(cell.Value, -1); match != nil {
return match[0][1], false
}
if rgxLine.MatchString(cell.Value) {
return "", true
}
}
return "", false
}
func getRangeProp(in *xlsx.Row) string {
if len(in.Cells) != 0 {
match := rangeRgx.FindAllStringSubmatch(in.Cells[0].Value, -1)
if match != nil {
return match[0][1]
}
}
return ""
}
func getRangeEndIndex(rows []*xlsx.Row) int {
var nesting int
for idx := 0; idx < len(rows); idx++ {
if len(rows[idx].Cells) == 0 {
continue
}
if rangeEndRgx.MatchString(rows[idx].Cells[0].Value) {
if nesting == 0 {
return idx
}
nesting--
continue
}
if rangeRgx.MatchString(rows[idx].Cells[0].Value) {
nesting++
}
}
return -1
}
func renderRow(in *xlsx.Row, ctx interface{}) error {
for _, cell := range in.Cells {
err := renderCell(cell, ctx)
if err != nil {
return err
}
}
return nil
}
1
https://gitee.com/lucky51/go-xlsx-templater.git
git@gitee.com:lucky51/go-xlsx-templater.git
lucky51
go-xlsx-templater
go-xlsx-templater
94cf377e1bb3

搜索帮助