comment.go 9.3 KB
Newer Older
xurime's avatar
xurime 已提交
1
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
xurime's avatar
xurime 已提交
2 3 4 5 6 7 8
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to
// and read from XLSX files. Support reads and writes XLSX file generated by
// Microsoft Excel™ 2007 and later. Support save file without losing original
// charts of XLSX. This library needs Go version 1.8 or later.
xurime's avatar
xurime 已提交
9

10 11 12 13 14
package excelize

import (
	"encoding/json"
	"encoding/xml"
R
Rad Cirskis 已提交
15
	"fmt"
16 17 18 19
	"strconv"
	"strings"
)

xurime's avatar
xurime 已提交
20 21
// parseFormatCommentsSet provides a function to parse the format settings of
// the comment with default value.
22
func parseFormatCommentsSet(formatSet string) (*formatComment, error) {
23 24 25 26
	format := formatComment{
		Author: "Author:",
		Text:   " ",
	}
27
	err := json.Unmarshal([]byte(formatSet), &format)
28
	return &format, err
29 30
}

xurime's avatar
xurime 已提交
31 32
// GetComments retrieves all comments and returns a map of worksheet name to
// the worksheet comments.
33 34
func (f *File) GetComments() (comments map[string][]Comment) {
	comments = map[string][]Comment{}
35
	for n := range f.sheetMap {
36
		if d := f.commentsReader("xl" + strings.TrimPrefix(f.getSheetComments(f.GetSheetIndex(n)), "..")); d != nil {
37 38 39 40 41 42 43 44 45 46 47 48 49 50
			sheetComments := []Comment{}
			for _, comment := range d.CommentList.Comment {
				sheetComment := Comment{}
				if comment.AuthorID < len(d.Authors) {
					sheetComment.Author = d.Authors[comment.AuthorID].Author
				}
				sheetComment.Ref = comment.Ref
				sheetComment.AuthorID = comment.AuthorID
				for _, text := range comment.Text.R {
					sheetComment.Text += text.T
				}
				sheetComments = append(sheetComments, sheetComment)
			}
			comments[n] = sheetComments
51 52 53 54 55
		}
	}
	return
}

56 57 58 59
// getSheetComments provides the method to get the target comment reference by
// given worksheet index.
func (f *File) getSheetComments(sheetID int) string {
	var rels = "xl/worksheets/_rels/sheet" + strconv.Itoa(sheetID) + ".xml.rels"
60 61 62 63 64
	if sheetRels := f.workSheetRelsReader(rels); sheetRels != nil {
		for _, v := range sheetRels.Relationships {
			if v.Type == SourceRelationshipComments {
				return v.Target
			}
65 66 67 68 69
		}
	}
	return ""
}

70
// AddComment provides the method to add comment in a sheet by given worksheet
71 72
// index, cell and format set (such as author and text). Note that the max
// author length is 255 and the max text length is 32512. For example, add a
73 74
// comment in Sheet1!$A$30:
//
75
//    xlsx.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`)
76
//
77 78 79 80 81
func (f *File) AddComment(sheet, cell, format string) error {
	formatSet, err := parseFormatCommentsSet(format)
	if err != nil {
		return err
	}
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
	// Read sheet data.
	xlsx := f.workSheetReader(sheet)
	commentID := f.countComments() + 1
	drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
	sheetRelationshipsComments := "../comments" + strconv.Itoa(commentID) + ".xml"
	sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
	if xlsx.LegacyDrawing != nil {
		// The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
		sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, xlsx.LegacyDrawing.RID)
		commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
		drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1)
	} else {
		// Add first comment for given sheet.
		rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
		f.addSheetRelationships(sheet, SourceRelationshipComments, sheetRelationshipsComments, "")
		f.addSheetLegacyDrawing(sheet, rID)
	}
	commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
	f.addComment(commentsXML, cell, formatSet)
R
Rad Cirskis 已提交
101 102 103 104 105 106 107 108 109 110
	var colCount int
	for i, l := range strings.Split(formatSet.Text, "\n") {
		if ll := len(l); ll > colCount {
			if i == 0 {
				ll += len(formatSet.Author)
			}
			colCount = ll
		}
	}
	f.addDrawingVML(commentID, drawingVML, cell, strings.Count(formatSet.Text, "\n")+1, colCount)
xurime's avatar
xurime 已提交
111
	f.addContentTypePart(commentID, "comments")
112
	return err
113 114
}

xurime's avatar
xurime 已提交
115
// addDrawingVML provides a function to create comment as
116
// xl/drawings/vmlDrawing%d.vml by given commit ID and cell.
R
Rad Cirskis 已提交
117
func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) {
118 119 120
	col := string(strings.Map(letterOnlyMapF, cell))
	row, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell))
	xAxis := row - 1
121
	yAxis := TitleToNumber(col)
122 123 124 125 126 127 128 129 130 131 132 133 134
	vml := f.VMLDrawing[drawingVML]
	if vml == nil {
		vml = &vmlDrawing{
			XMLNSv:  "urn:schemas-microsoft-com:vml",
			XMLNSo:  "urn:schemas-microsoft-com:office:office",
			XMLNSx:  "urn:schemas-microsoft-com:office:excel",
			XMLNSmv: "http://macVmlSchemaUri",
			Shapelayout: &xlsxShapelayout{
				Ext: "edit",
				IDmap: &xlsxIDmap{
					Ext:  "edit",
					Data: commentID,
				},
135
			},
136 137 138 139 140 141 142 143 144 145 146 147
			Shapetype: &xlsxShapetype{
				ID:        "_x0000_t202",
				Coordsize: "21600,21600",
				Spt:       202,
				Path:      "m0,0l0,21600,21600,21600,21600,0xe",
				Stroke: &xlsxStroke{
					Joinstyle: "miter",
				},
				VPath: &vPath{
					Gradientshapeok: "t",
					Connecttype:     "miter",
				},
148
			},
149
		}
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
	}
	sp := encodeShape{
		Fill: &vFill{
			Color2: "#fbfe82",
			Angle:  -180,
			Type:   "gradient",
			Fill: &oFill{
				Ext:  "view",
				Type: "gradientUnscaled",
			},
		},
		Shadow: &vShadow{
			On:       "t",
			Color:    "black",
			Obscured: "t",
		},
		Path: &vPath{
			Connecttype: "none",
		},
		Textbox: &vTextbox{
			Style: "mso-direction-alt:auto",
			Div: &xlsxDiv{
				Style: "text-align:left",
			},
		},
		ClientData: &xClientData{
			ObjectType: "Note",
R
Rad Cirskis 已提交
177 178
			Anchor: fmt.Sprintf(
				"%d, 23, %d, 0, %d, %d, %d, 5",
xurime's avatar
xurime 已提交
179
				1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount),
R
Rad Cirskis 已提交
180 181 182
			AutoFill: "True",
			Row:      xAxis,
			Column:   yAxis,
183 184 185 186 187 188 189 190 191 192 193
		},
	}
	s, _ := xml.Marshal(sp)
	shape := xlsxShape{
		ID:          "_x0000_s1025",
		Type:        "#_x0000_t202",
		Style:       "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
		Fillcolor:   "#fbf6d6",
		Strokecolor: "#edeaa1",
		Val:         string(s[13 : len(s)-14]),
	}
194 195
	d := f.decodeVMLDrawingReader(drawingVML)
	if d != nil {
196 197 198 199 200 201 202 203 204 205 206 207 208
		for _, v := range d.Shape {
			s := xlsxShape{
				ID:          "_x0000_s1025",
				Type:        "#_x0000_t202",
				Style:       "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
				Fillcolor:   "#fbf6d6",
				Strokecolor: "#edeaa1",
				Val:         v.Val,
			}
			vml.Shape = append(vml.Shape, s)
		}
	}
	vml.Shape = append(vml.Shape, shape)
209
	f.VMLDrawing[drawingVML] = vml
210 211
}

xurime's avatar
xurime 已提交
212 213
// addComment provides a function to create chart as xl/comments%d.xml by
// given cell and format sets.
214
func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) {
215 216 217 218 219 220 221 222
	a := formatSet.Author
	t := formatSet.Text
	if len(a) > 255 {
		a = a[0:255]
	}
	if len(t) > 32512 {
		t = t[0:32512]
	}
223 224 225 226 227 228 229
	comments := f.commentsReader(commentsXML)
	if comments == nil {
		comments = &xlsxComments{
			Authors: []xlsxAuthor{
				{
					Author: formatSet.Author,
				},
230
			},
231
		}
232 233 234 235 236 237
	}
	cmt := xlsxComment{
		Ref:      cell,
		AuthorID: 0,
		Text: xlsxText{
			R: []xlsxR{
238
				{
239 240
					RPr: &xlsxRPr{
						B:  " ",
xurime's avatar
xurime 已提交
241
						Sz: &attrValFloat{Val: 9},
242 243 244 245 246 247
						Color: &xlsxColor{
							Indexed: 81,
						},
						RFont:  &attrValString{Val: "Calibri"},
						Family: &attrValInt{Val: 2},
					},
248
					T: a,
249
				},
250
				{
251
					RPr: &xlsxRPr{
xurime's avatar
xurime 已提交
252
						Sz: &attrValFloat{Val: 9},
253 254 255 256 257 258
						Color: &xlsxColor{
							Indexed: 81,
						},
						RFont:  &attrValString{Val: "Calibri"},
						Family: &attrValInt{Val: 2},
					},
259
					T: t,
260 261 262 263 264
				},
			},
		},
	}
	comments.CommentList.Comment = append(comments.CommentList.Comment, cmt)
265
	f.Comments[commentsXML] = comments
266 267
}

xurime's avatar
xurime 已提交
268 269
// countComments provides a function to get comments files count storage in
// the folder xl.
270 271 272 273 274 275 276 277 278
func (f *File) countComments() int {
	count := 0
	for k := range f.XLSX {
		if strings.Contains(k, "xl/comments") {
			count++
		}
	}
	return count
}
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293

// decodeVMLDrawingReader provides a function to get the pointer to the
// structure after deserialization of xl/drawings/vmlDrawing%d.xml.
func (f *File) decodeVMLDrawingReader(path string) *decodeVmlDrawing {
	if f.DecodeVMLDrawing[path] == nil {
		c, ok := f.XLSX[path]
		if ok {
			d := decodeVmlDrawing{}
			_ = xml.Unmarshal(namespaceStrictToTransitional(c), &d)
			f.DecodeVMLDrawing[path] = &d
		}
	}
	return f.DecodeVMLDrawing[path]
}

294
// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
// after serialize structure.
func (f *File) vmlDrawingWriter() {
	for path, vml := range f.VMLDrawing {
		if vml != nil {
			v, _ := xml.Marshal(vml)
			f.XLSX[path] = v
		}
	}
}

// commentsReader provides a function to get the pointer to the structure
// after deserialization of xl/comments%d.xml.
func (f *File) commentsReader(path string) *xlsxComments {
	if f.Comments[path] == nil {
		content, ok := f.XLSX[path]
		if ok {
			c := xlsxComments{}
			_ = xml.Unmarshal(namespaceStrictToTransitional(content), &c)
			f.Comments[path] = &c
		}
	}
	return f.Comments[path]
}

// commentsWriter provides a function to save xl/comments%d.xml after
// serialize structure.
func (f *File) commentsWriter() {
	for path, c := range f.Comments {
		if c != nil {
			v, _ := xml.Marshal(c)
			f.saveFileList(path, v)
		}
	}
}