2019-12-29 16:02:31 +08:00
|
|
|
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
|
2018-09-14 00:44:23 +08:00
|
|
|
// 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
|
2020-06-22 00:14:56 +08:00
|
|
|
// and read from XLSX / XLSM / XLTM files. Supports reading and writing
|
|
|
|
// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
|
|
|
|
// complex components by high compatibility, and provided streaming API for
|
|
|
|
// generating or reading data from a worksheet with huge amounts of data. This
|
|
|
|
// library needs Go version 1.10 or later.
|
2018-09-14 00:58:48 +08:00
|
|
|
|
2017-05-13 13:28:21 +08:00
|
|
|
package excelize
|
|
|
|
|
|
|
|
import (
|
2019-12-20 00:30:48 +08:00
|
|
|
"bytes"
|
2017-05-13 13:28:21 +08:00
|
|
|
"encoding/json"
|
|
|
|
"encoding/xml"
|
2018-06-23 19:35:27 +08:00
|
|
|
"fmt"
|
2019-12-20 00:30:48 +08:00
|
|
|
"io"
|
|
|
|
"log"
|
2020-03-31 00:02:00 +08:00
|
|
|
"path/filepath"
|
2017-05-13 13:28:21 +08:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// parseFormatCommentsSet provides a function to parse the format settings of
|
|
|
|
// the comment with default value.
|
2018-05-27 11:25:55 +08:00
|
|
|
func parseFormatCommentsSet(formatSet string) (*formatComment, error) {
|
2017-05-13 13:28:21 +08:00
|
|
|
format := formatComment{
|
|
|
|
Author: "Author:",
|
|
|
|
Text: " ",
|
|
|
|
}
|
2018-07-17 15:28:22 +08:00
|
|
|
err := json.Unmarshal([]byte(formatSet), &format)
|
2018-05-27 11:25:55 +08:00
|
|
|
return &format, err
|
2017-05-13 13:28:21 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// GetComments retrieves all comments and returns a map of worksheet name to
|
|
|
|
// the worksheet comments.
|
2018-09-14 00:24:49 +08:00
|
|
|
func (f *File) GetComments() (comments map[string][]Comment) {
|
|
|
|
comments = map[string][]Comment{}
|
2020-03-31 00:02:00 +08:00
|
|
|
for n, path := range f.sheetMap {
|
|
|
|
if d := f.commentsReader("xl" + strings.TrimPrefix(f.getSheetComments(filepath.Base(path)), "..")); d != nil {
|
2018-09-14 00:24:49 +08:00
|
|
|
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
|
2019-07-15 09:13:55 +08:00
|
|
|
if comment.Text.T != nil {
|
|
|
|
sheetComment.Text += *comment.Text.T
|
|
|
|
}
|
2018-09-14 00:24:49 +08:00
|
|
|
for _, text := range comment.Text.R {
|
2020-04-06 00:23:27 +08:00
|
|
|
if text.T != nil {
|
|
|
|
sheetComment.Text += text.T.Val
|
|
|
|
}
|
2018-09-14 00:24:49 +08:00
|
|
|
}
|
|
|
|
sheetComments = append(sheetComments, sheetComment)
|
|
|
|
}
|
|
|
|
comments[n] = sheetComments
|
2018-06-30 18:37:14 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-02-23 16:20:44 +08:00
|
|
|
// getSheetComments provides the method to get the target comment reference by
|
2020-03-31 00:02:00 +08:00
|
|
|
// given worksheet file path.
|
|
|
|
func (f *File) getSheetComments(sheetFile string) string {
|
|
|
|
var rels = "xl/worksheets/_rels/" + sheetFile + ".rels"
|
2019-09-16 01:17:35 +08:00
|
|
|
if sheetRels := f.relsReader(rels); sheetRels != nil {
|
2019-02-26 14:21:44 +08:00
|
|
|
for _, v := range sheetRels.Relationships {
|
|
|
|
if v.Type == SourceRelationshipComments {
|
|
|
|
return v.Target
|
|
|
|
}
|
2019-02-23 16:20:44 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2017-05-13 13:28:21 +08:00
|
|
|
// AddComment provides the method to add comment in a sheet by given worksheet
|
2017-05-13 14:12:43 +08:00
|
|
|
// 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
|
2017-05-13 13:28:21 +08:00
|
|
|
// comment in Sheet1!$A$30:
|
|
|
|
//
|
2019-04-21 00:04:42 +08:00
|
|
|
// err := f.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`)
|
2017-05-13 13:28:21 +08:00
|
|
|
//
|
2018-05-27 11:25:55 +08:00
|
|
|
func (f *File) AddComment(sheet, cell, format string) error {
|
|
|
|
formatSet, err := parseFormatCommentsSet(format)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-05-13 13:28:21 +08:00
|
|
|
// Read sheet data.
|
2020-11-10 23:48:09 +08:00
|
|
|
ws, err := f.workSheetReader(sheet)
|
2019-04-15 11:22:57 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-05-13 13:28:21 +08:00
|
|
|
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"
|
2020-11-10 23:48:09 +08:00
|
|
|
if ws.LegacyDrawing != nil {
|
2017-05-13 13:28:21 +08:00
|
|
|
// The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
|
2020-11-10 23:48:09 +08:00
|
|
|
sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID)
|
2017-05-13 13:28:21 +08:00
|
|
|
commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
|
|
|
|
drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1)
|
|
|
|
} else {
|
|
|
|
// Add first comment for given sheet.
|
2019-12-23 00:07:40 +08:00
|
|
|
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels"
|
2019-09-16 01:17:35 +08:00
|
|
|
rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
|
|
|
|
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
|
2020-07-18 15:15:16 +08:00
|
|
|
f.addSheetNameSpace(sheet, SourceRelationship)
|
2017-05-13 13:28:21 +08:00
|
|
|
f.addSheetLegacyDrawing(sheet, rID)
|
|
|
|
}
|
|
|
|
commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
|
2018-06-23 19:35:27 +08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2019-03-23 20:08:06 +08:00
|
|
|
err = f.addDrawingVML(commentID, drawingVML, cell, strings.Count(formatSet.Text, "\n")+1, colCount)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-03-31 00:02:00 +08:00
|
|
|
f.addComment(commentsXML, cell, formatSet)
|
2017-05-24 14:17:35 +08:00
|
|
|
f.addContentTypePart(commentID, "comments")
|
2018-05-27 11:25:55 +08:00
|
|
|
return err
|
2017-05-13 13:28:21 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// addDrawingVML provides a function to create comment as
|
2017-05-13 13:28:21 +08:00
|
|
|
// xl/drawings/vmlDrawing%d.vml by given commit ID and cell.
|
2019-03-23 20:08:06 +08:00
|
|
|
func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) error {
|
|
|
|
col, row, err := CellNameToCoordinates(cell)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
Huge refactorig for consistent col/row numbering (#356)
* Huge refactorig for consistent col/row numbering
Started from simply changing ToALphaString()/TitleToNumber() logic and related fixes.
But have to go deeper, do fixes, after do related fixes and again and again.
Major improvements:
1. Tests made stronger again (But still be weak).
2. "Empty" returns for incorrect input replaces with panic.
3. Check for correct col/row/cell naming & addressing by default.
4. Removed huge amount of duplicated code.
5. Removed ToALphaString(), TitleToNumber() and it helpers functions at all,
and replaced with SplitCellName(), JoinCellName(), ColumnNameToNumber(), ColumnNumberToName(), CellNameToCoordinates(), CoordinatesToCellName().
6. Minor fixes for internal variable naming for code readability (ex. col, row for input params, colIdx, rowIdx for slice indexes etc).
* Formatting fixes
2019-03-20 00:14:41 +08:00
|
|
|
yAxis := col - 1
|
2017-05-13 13:28:21 +08:00
|
|
|
xAxis := row - 1
|
2019-02-25 00:29:58 +08:00
|
|
|
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,
|
|
|
|
},
|
2017-05-13 13:28:21 +08:00
|
|
|
},
|
2019-02-25 00:29:58 +08:00
|
|
|
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",
|
2020-07-22 20:20:00 +08:00
|
|
|
Connecttype: "rect",
|
2019-02-25 00:29:58 +08:00
|
|
|
},
|
2017-05-13 13:28:21 +08:00
|
|
|
},
|
2019-02-25 00:29:58 +08:00
|
|
|
}
|
2017-05-13 13:28:21 +08:00
|
|
|
}
|
|
|
|
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",
|
2018-06-23 19:35:27 +08:00
|
|
|
Anchor: fmt.Sprintf(
|
|
|
|
"%d, 23, %d, 0, %d, %d, %d, 5",
|
2018-06-28 10:03:53 +08:00
|
|
|
1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount),
|
2018-06-23 19:35:27 +08:00
|
|
|
AutoFill: "True",
|
|
|
|
Row: xAxis,
|
|
|
|
Column: yAxis,
|
2017-05-13 13:28:21 +08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
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]),
|
|
|
|
}
|
2019-02-25 00:29:58 +08:00
|
|
|
d := f.decodeVMLDrawingReader(drawingVML)
|
|
|
|
if d != nil {
|
2017-05-13 13:28:21 +08:00
|
|
|
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)
|
2019-02-25 00:29:58 +08:00
|
|
|
f.VMLDrawing[drawingVML] = vml
|
2019-03-23 20:08:06 +08:00
|
|
|
return err
|
2017-05-13 13:28:21 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// addComment provides a function to create chart as xl/comments%d.xml by
|
|
|
|
// given cell and format sets.
|
2017-05-13 13:28:21 +08:00
|
|
|
func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) {
|
2017-05-13 14:12:43 +08:00
|
|
|
a := formatSet.Author
|
|
|
|
t := formatSet.Text
|
|
|
|
if len(a) > 255 {
|
|
|
|
a = a[0:255]
|
|
|
|
}
|
|
|
|
if len(t) > 32512 {
|
|
|
|
t = t[0:32512]
|
|
|
|
}
|
2019-02-25 00:29:58 +08:00
|
|
|
comments := f.commentsReader(commentsXML)
|
|
|
|
if comments == nil {
|
|
|
|
comments = &xlsxComments{
|
|
|
|
Authors: []xlsxAuthor{
|
|
|
|
{
|
|
|
|
Author: formatSet.Author,
|
|
|
|
},
|
2017-05-13 13:28:21 +08:00
|
|
|
},
|
2019-02-25 00:29:58 +08:00
|
|
|
}
|
2017-05-13 13:28:21 +08:00
|
|
|
}
|
2019-04-26 00:24:25 +08:00
|
|
|
defaultFont := f.GetDefaultFont()
|
2017-05-13 13:28:21 +08:00
|
|
|
cmt := xlsxComment{
|
|
|
|
Ref: cell,
|
|
|
|
AuthorID: 0,
|
|
|
|
Text: xlsxText{
|
|
|
|
R: []xlsxR{
|
2017-05-16 20:42:01 +08:00
|
|
|
{
|
2017-05-13 13:28:21 +08:00
|
|
|
RPr: &xlsxRPr{
|
|
|
|
B: " ",
|
2019-12-23 00:07:40 +08:00
|
|
|
Sz: &attrValFloat{Val: float64Ptr(9)},
|
2017-05-13 13:28:21 +08:00
|
|
|
Color: &xlsxColor{
|
|
|
|
Indexed: 81,
|
|
|
|
},
|
2019-12-23 00:07:40 +08:00
|
|
|
RFont: &attrValString{Val: stringPtr(defaultFont)},
|
|
|
|
Family: &attrValInt{Val: intPtr(2)},
|
2017-05-13 13:28:21 +08:00
|
|
|
},
|
2020-04-06 00:23:27 +08:00
|
|
|
T: &xlsxT{Val: a},
|
2017-05-13 13:28:21 +08:00
|
|
|
},
|
2017-05-16 20:42:01 +08:00
|
|
|
{
|
2017-05-13 13:28:21 +08:00
|
|
|
RPr: &xlsxRPr{
|
2019-12-23 00:07:40 +08:00
|
|
|
Sz: &attrValFloat{Val: float64Ptr(9)},
|
2017-05-13 13:28:21 +08:00
|
|
|
Color: &xlsxColor{
|
|
|
|
Indexed: 81,
|
|
|
|
},
|
2019-12-23 00:07:40 +08:00
|
|
|
RFont: &attrValString{Val: stringPtr(defaultFont)},
|
|
|
|
Family: &attrValInt{Val: intPtr(2)},
|
2017-05-13 13:28:21 +08:00
|
|
|
},
|
2020-04-06 00:23:27 +08:00
|
|
|
T: &xlsxT{Val: t},
|
2017-05-13 13:28:21 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
comments.CommentList.Comment = append(comments.CommentList.Comment, cmt)
|
2019-02-25 00:29:58 +08:00
|
|
|
f.Comments[commentsXML] = comments
|
2017-05-13 13:28:21 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// countComments provides a function to get comments files count storage in
|
|
|
|
// the folder xl.
|
2017-05-13 13:28:21 +08:00
|
|
|
func (f *File) countComments() int {
|
2019-05-11 09:46:20 +08:00
|
|
|
c1, c2 := 0, 0
|
2017-05-13 13:28:21 +08:00
|
|
|
for k := range f.XLSX {
|
|
|
|
if strings.Contains(k, "xl/comments") {
|
2019-05-11 09:46:20 +08:00
|
|
|
c1++
|
2017-05-13 13:28:21 +08:00
|
|
|
}
|
|
|
|
}
|
2019-05-11 09:46:20 +08:00
|
|
|
for rel := range f.Comments {
|
|
|
|
if strings.Contains(rel, "xl/comments") {
|
|
|
|
c2++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if c1 < c2 {
|
|
|
|
return c2
|
|
|
|
}
|
|
|
|
return c1
|
2017-05-13 13:28:21 +08:00
|
|
|
}
|
2019-02-25 00:29:58 +08:00
|
|
|
|
|
|
|
// 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 {
|
2019-12-20 00:30:48 +08:00
|
|
|
var err error
|
|
|
|
|
2019-02-25 00:29:58 +08:00
|
|
|
if f.DecodeVMLDrawing[path] == nil {
|
|
|
|
c, ok := f.XLSX[path]
|
|
|
|
if ok {
|
2019-12-20 00:30:48 +08:00
|
|
|
f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
|
|
|
|
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c))).
|
|
|
|
Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
|
|
|
|
log.Printf("xml decode error: %s", err)
|
|
|
|
}
|
2019-02-25 00:29:58 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return f.DecodeVMLDrawing[path]
|
|
|
|
}
|
|
|
|
|
2019-02-25 22:14:34 +08:00
|
|
|
// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml
|
2019-02-25 00:29:58 +08:00
|
|
|
// 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 {
|
2019-12-20 00:30:48 +08:00
|
|
|
var err error
|
|
|
|
|
2019-02-25 00:29:58 +08:00
|
|
|
if f.Comments[path] == nil {
|
|
|
|
content, ok := f.XLSX[path]
|
|
|
|
if ok {
|
2019-12-20 00:30:48 +08:00
|
|
|
f.Comments[path] = new(xlsxComments)
|
|
|
|
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content))).
|
|
|
|
Decode(f.Comments[path]); err != nil && err != io.EOF {
|
|
|
|
log.Printf("xml decode error: %s", err)
|
|
|
|
}
|
2019-02-25 00:29:58 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|