comment.go 9.5 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
// 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
8
// charts of XLSX. This library needs Go version 1.10 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
			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
xurime's avatar
xurime 已提交
45 46 47
				if comment.Text.T != nil {
					sheetComment.Text += *comment.Text.T
				}
48 49 50 51 52 53
				for _, text := range comment.Text.R {
					sheetComment.Text += text.T
				}
				sheetComments = append(sheetComments, sheetComment)
			}
			comments[n] = sheetComments
54 55 56 57 58
		}
	}
	return
}

59 60 61 62
// 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"
63 64 65 66 67
	if sheetRels := f.workSheetRelsReader(rels); sheetRels != nil {
		for _, v := range sheetRels.Relationships {
			if v.Type == SourceRelationshipComments {
				return v.Target
			}
68 69 70 71 72
		}
	}
	return ""
}

73
// AddComment provides the method to add comment in a sheet by given worksheet
74 75
// 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
76 77
// comment in Sheet1!$A$30:
//
78
//    err := f.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`)
79
//
80 81 82 83 84
func (f *File) AddComment(sheet, cell, format string) error {
	formatSet, err := parseFormatCommentsSet(format)
	if err != nil {
		return err
	}
85
	// Read sheet data.
xurime's avatar
xurime 已提交
86 87 88 89
	xlsx, err := f.workSheetReader(sheet)
	if err != nil {
		return err
	}
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
	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 已提交
107 108 109 110 111 112 113 114 115
	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
		}
	}
116 117 118 119
	err = f.addDrawingVML(commentID, drawingVML, cell, strings.Count(formatSet.Text, "\n")+1, colCount)
	if err != nil {
		return err
	}
xurime's avatar
xurime 已提交
120
	f.addContentTypePart(commentID, "comments")
121
	return err
122 123
}

xurime's avatar
xurime 已提交
124
// addDrawingVML provides a function to create comment as
125
// xl/drawings/vmlDrawing%d.vml by given commit ID and cell.
126 127 128 129 130
func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) error {
	col, row, err := CellNameToCoordinates(cell)
	if err != nil {
		return err
	}
131
	yAxis := col - 1
132
	xAxis := row - 1
133 134 135 136 137 138 139 140 141 142 143 144 145
	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,
				},
146
			},
147 148 149 150 151 152 153 154 155 156 157 158
			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",
				},
159
			},
160
		}
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
	}
	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 已提交
188 189
			Anchor: fmt.Sprintf(
				"%d, 23, %d, 0, %d, %d, %d, 5",
xurime's avatar
xurime 已提交
190
				1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount),
R
Rad Cirskis 已提交
191 192 193
			AutoFill: "True",
			Row:      xAxis,
			Column:   yAxis,
194 195 196 197 198 199 200 201 202 203 204
		},
	}
	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]),
	}
205 206
	d := f.decodeVMLDrawingReader(drawingVML)
	if d != nil {
207 208 209 210 211 212 213 214 215 216 217 218 219
		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)
220
	f.VMLDrawing[drawingVML] = vml
221
	return err
222 223
}

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

xurime's avatar
xurime 已提交
281 282
// countComments provides a function to get comments files count storage in
// the folder xl.
283
func (f *File) countComments() int {
284
	c1, c2 := 0, 0
285 286
	for k := range f.XLSX {
		if strings.Contains(k, "xl/comments") {
287
			c1++
288 289
		}
	}
290 291 292 293 294 295 296 297 298
	for rel := range f.Comments {
		if strings.Contains(rel, "xl/comments") {
			c2++
		}
	}
	if c1 < c2 {
		return c2
	}
	return c1
299
}
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314

// 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]
}

315
// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
// 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)
		}
	}
}