This closes #1204, breaking changes for add comments

- Allowing insert SVG format images
- Unit tests updated
This commit is contained in:
xuri 2022-11-02 08:42:00 +08:00
parent a410b22bdd
commit db2d084ada
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
10 changed files with 181 additions and 137 deletions

18
.gitignore vendored
View File

@ -1,15 +1,15 @@
.DS_Store
.idea
*.json
*.out
*.test
~$*.xlsx
test/*.png
test/BadWorkbook.SaveAsEmptyStruct.xlsx
test/Encryption*.xlsx
test/excelize-*
test/Test*.xlam
test/Test*.xlsm
test/Test*.xlsx
test/Test*.xltm
test/Test*.xltx
# generated files
test/Encryption*.xlsx
test/BadWorkbook.SaveAsEmptyStruct.xlsx
test/*.png
test/excelize-*
*.out
*.test
.idea
.DS_Store

55
cell.go
View File

@ -902,31 +902,7 @@ func getCellRichText(si *xlsxSI) (runs []RichTextRun) {
Text: v.T.Val,
}
if v.RPr != nil {
font := Font{Underline: "none"}
font.Bold = v.RPr.B != nil
font.Italic = v.RPr.I != nil
if v.RPr.U != nil {
font.Underline = "single"
if v.RPr.U.Val != nil {
font.Underline = *v.RPr.U.Val
}
}
if v.RPr.RFont != nil && v.RPr.RFont.Val != nil {
font.Family = *v.RPr.RFont.Val
}
if v.RPr.Sz != nil && v.RPr.Sz.Val != nil {
font.Size = *v.RPr.Sz.Val
}
font.Strike = v.RPr.Strike != nil
if v.RPr.Color != nil {
font.Color = strings.TrimPrefix(v.RPr.Color.RGB, "FF")
if v.RPr.Color.Theme != nil {
font.ColorTheme = v.RPr.Color.Theme
}
font.ColorIndexed = v.RPr.Color.Indexed
font.ColorTint = v.RPr.Color.Tint
}
run.Font = &font
run.Font = newFont(v.RPr)
}
runs = append(runs, run)
}
@ -985,6 +961,35 @@ func newRpr(fnt *Font) *xlsxRPr {
return &rpr
}
// newFont create font format by given run properties for the rich text.
func newFont(rPr *xlsxRPr) *Font {
font := Font{Underline: "none"}
font.Bold = rPr.B != nil
font.Italic = rPr.I != nil
if rPr.U != nil {
font.Underline = "single"
if rPr.U.Val != nil {
font.Underline = *rPr.U.Val
}
}
if rPr.RFont != nil && rPr.RFont.Val != nil {
font.Family = *rPr.RFont.Val
}
if rPr.Sz != nil && rPr.Sz.Val != nil {
font.Size = *rPr.Sz.Val
}
font.Strike = rPr.Strike != nil
if rPr.Color != nil {
font.Color = strings.TrimPrefix(rPr.Color.RGB, "FF")
if rPr.Color.Theme != nil {
font.ColorTheme = rPr.Color.Theme
}
font.ColorIndexed = rPr.Color.Indexed
font.ColorTint = rPr.Color.Tint
}
return &font
}
// setRichText provides a function to set rich text of a cell.
func setRichText(runs []RichTextRun) ([]xlsxR, error) {
var (

View File

@ -13,7 +13,6 @@ package excelize
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io"
@ -23,17 +22,6 @@ import (
"strings"
)
// parseCommentOptions provides a function to parse the format settings of
// the comment with default value.
func parseCommentOptions(opts string) (*commentOptions, error) {
options := commentOptions{
Author: "Author:",
Text: " ",
}
err := json.Unmarshal([]byte(opts), &options)
return &options, err
}
// GetComments retrieves all comments and returns a map of worksheet name to
// the worksheet comments.
func (f *File) GetComments() (comments map[string][]Comment) {
@ -53,14 +41,18 @@ func (f *File) GetComments() (comments map[string][]Comment) {
if comment.AuthorID < len(d.Authors.Author) {
sheetComment.Author = d.Authors.Author[comment.AuthorID]
}
sheetComment.Ref = comment.Ref
sheetComment.Cell = comment.Ref
sheetComment.AuthorID = comment.AuthorID
if comment.Text.T != nil {
sheetComment.Text += *comment.Text.T
}
for _, text := range comment.Text.R {
if text.T != nil {
sheetComment.Text += text.T.Val
run := RichTextRun{Text: text.T.Val}
if text.RPr != nil {
run.Font = newFont(text.RPr)
}
sheetComment.Runs = append(sheetComment.Runs, run)
}
}
sheetComments = append(sheetComments, sheetComment)
@ -92,12 +84,15 @@ func (f *File) getSheetComments(sheetFile string) string {
// author length is 255 and the max text length is 32512. For example, add a
// comment in Sheet1!$A$30:
//
// err := f.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`)
func (f *File) AddComment(sheet, cell, opts string) error {
options, err := parseCommentOptions(opts)
if err != nil {
return err
}
// err := f.AddComment(sheet, excelize.Comment{
// Cell: "A12",
// Author: "Excelize",
// Runs: []excelize.RichTextRun{
// {Text: "Excelize: ", Font: &excelize.Font{Bold: true}},
// {Text: "This is a comment."},
// },
// })
func (f *File) AddComment(sheet string, comment Comment) error {
// Read sheet data.
ws, err := f.workSheetReader(sheet)
if err != nil {
@ -122,20 +117,19 @@ func (f *File) AddComment(sheet, cell, opts string) error {
f.addSheetLegacyDrawing(sheet, rID)
}
commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
var colCount int
for i, l := range strings.Split(options.Text, "\n") {
if ll := len(l); ll > colCount {
if i == 0 {
ll += len(options.Author)
}
colCount = ll
var rows, cols int
for _, runs := range comment.Runs {
for _, subStr := range strings.Split(runs.Text, "\n") {
rows++
if chars := len(subStr); chars > cols {
cols = chars
}
}
err = f.addDrawingVML(commentID, drawingVML, cell, strings.Count(options.Text, "\n")+1, colCount)
if err != nil {
}
if err = f.addDrawingVML(commentID, drawingVML, comment.Cell, rows+1, cols); err != nil {
return err
}
f.addComment(commentsXML, cell, options)
f.addComment(commentsXML, comment)
f.addContentTypePart(commentID, "comments")
return err
}
@ -280,44 +274,42 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount,
// addComment provides a function to create chart as xl/comments%d.xml by
// given cell and format sets.
func (f *File) addComment(commentsXML, cell string, opts *commentOptions) {
a := opts.Author
t := opts.Text
if len(a) > MaxFieldLength {
a = a[:MaxFieldLength]
func (f *File) addComment(commentsXML string, comment Comment) {
if comment.Author == "" {
comment.Author = "Author"
}
if len(t) > 32512 {
t = t[:32512]
if len(comment.Author) > MaxFieldLength {
comment.Author = comment.Author[:MaxFieldLength]
}
comments := f.commentsReader(commentsXML)
authorID := 0
comments, authorID := f.commentsReader(commentsXML), 0
if comments == nil {
comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{opts.Author}}}
comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{comment.Author}}}
}
if inStrSlice(comments.Authors.Author, opts.Author, true) == -1 {
comments.Authors.Author = append(comments.Authors.Author, opts.Author)
if inStrSlice(comments.Authors.Author, comment.Author, true) == -1 {
comments.Authors.Author = append(comments.Authors.Author, comment.Author)
authorID = len(comments.Authors.Author) - 1
}
defaultFont := f.GetDefaultFont()
bold := ""
cmt := xlsxComment{
Ref: cell,
defaultFont, chars, cmt := f.GetDefaultFont(), 0, xlsxComment{
Ref: comment.Cell,
AuthorID: authorID,
Text: xlsxText{
R: []xlsxR{
{
RPr: &xlsxRPr{
B: &bold,
Sz: &attrValFloat{Val: float64Ptr(9)},
Color: &xlsxColor{
Indexed: 81,
},
RFont: &attrValString{Val: stringPtr(defaultFont)},
Family: &attrValInt{Val: intPtr(2)},
},
T: &xlsxT{Val: a},
},
{
Text: xlsxText{R: []xlsxR{}},
}
if comment.Text != "" {
if len(comment.Text) > TotalCellChars {
comment.Text = comment.Text[:TotalCellChars]
}
cmt.Text.T = stringPtr(comment.Text)
chars += len(comment.Text)
}
for _, run := range comment.Runs {
if chars == TotalCellChars {
break
}
if chars+len(run.Text) > TotalCellChars {
run.Text = run.Text[:TotalCellChars-chars]
}
chars += len(run.Text)
r := xlsxR{
RPr: &xlsxRPr{
Sz: &attrValFloat{Val: float64Ptr(9)},
Color: &xlsxColor{
@ -326,10 +318,15 @@ func (f *File) addComment(commentsXML, cell string, opts *commentOptions) {
RFont: &attrValString{Val: stringPtr(defaultFont)},
Family: &attrValInt{Val: intPtr(2)},
},
T: &xlsxT{Val: t},
},
},
},
T: &xlsxT{Val: run.Text, Space: xml.Attr{
Name: xml.Name{Space: NameSpaceXML, Local: "space"},
Value: "preserve",
}},
}
if run.Font != nil {
r.RPr = newRpr(run.Font)
}
cmt.Text.R = append(cmt.Text.R, r)
}
comments.CommentList.Comment = append(comments.CommentList.Comment, cmt)
f.Comments[commentsXML] = comments

View File

@ -26,14 +26,14 @@ func TestAddComments(t *testing.T) {
t.FailNow()
}
s := strings.Repeat("c", 32768)
assert.NoError(t, f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`))
assert.NoError(t, f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`))
s := strings.Repeat("c", TotalCellChars+1)
assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Runs: []RichTextRun{{Text: s}, {Text: s}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
// Test add comment on not exists worksheet.
assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN does not exist")
assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist")
// Test add comment on with illegal cell reference
assert.EqualError(t, f.AddComment("Sheet1", "A", `{"author":"Excelize: ","text":"This is a comment."}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) {
assert.Len(t, f.GetComments(), 2)
}
@ -52,12 +52,12 @@ func TestDeleteComment(t *testing.T) {
t.FailNow()
}
assert.NoError(t, f.AddComment("Sheet2", "A40", `{"author":"Excelize: ","text":"This is a comment1."}`))
assert.NoError(t, f.AddComment("Sheet2", "A41", `{"author":"Excelize: ","text":"This is a comment2."}`))
assert.NoError(t, f.AddComment("Sheet2", "C41", `{"author":"Excelize: ","text":"This is a comment3."}`))
assert.NoError(t, f.AddComment("Sheet2", "C41", `{"author":"Excelize: ","text":"This is a comment3-1."}`))
assert.NoError(t, f.AddComment("Sheet2", "C42", `{"author":"Excelize: ","text":"This is a comment4."}`))
assert.NoError(t, f.AddComment("Sheet2", "C41", `{"author":"Excelize: ","text":"This is a comment3-2."}`))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A40", Text: "Excelize: This is a comment1."}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3-1."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C42", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment4."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
assert.NoError(t, f.DeleteComment("Sheet2", "A40"))

View File

@ -946,8 +946,7 @@ func TestSetDeleteSheet(t *testing.T) {
t.FailNow()
}
f.DeleteSheet("Sheet1")
assert.EqualError(t, f.AddComment("Sheet1", "A1", ""), "unexpected end of JSON input")
assert.NoError(t, f.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`))
assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx")))
})
}

View File

@ -183,7 +183,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, opts, name, extension string, fi
drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, options.Hyperlink, hyperlinkType)
}
ws.Unlock()
err = f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, options)
err = f.addDrawingPicture(sheet, drawingXML, cell, name, ext, drawingRID, drawingHyperlinkRID, img, options)
if err != nil {
return err
}
@ -263,11 +263,12 @@ func (f *File) countDrawings() (count int) {
// addDrawingPicture provides a function to add picture by given sheet,
// drawingXML, cell, file name, width, height relationship index and format
// sets.
func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, height, rID, hyperlinkRID int, opts *pictureOptions) error {
func (f *File) addDrawingPicture(sheet, drawingXML, cell, file, ext string, rID, hyperlinkRID int, img image.Config, opts *pictureOptions) error {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
width, height := img.Width, img.Height
if opts.Autofit {
width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts)
if err != nil {
@ -308,6 +309,19 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he
}
pic.BlipFill.Blip.R = SourceRelationship.Value
pic.BlipFill.Blip.Embed = "rId" + strconv.Itoa(rID)
if ext == ".svg" {
pic.BlipFill.Blip.ExtList = &xlsxEGOfficeArtExtensionList{
Ext: []xlsxCTOfficeArtExtension{
{
URI: ExtURISVG,
SVGBlip: xlsxCTSVGBlip{
XMLNSaAVG: NameSpaceDrawing2016SVG.Value,
Embed: pic.BlipFill.Blip.Embed,
},
},
},
}
}
pic.SpPr.PrstGeom.Prst = "rect"
twoCellAnchor.Pic = &pic
@ -362,7 +376,10 @@ func (f *File) addMedia(file []byte, ext string) string {
// setContentTypePartImageExtensions provides a function to set the content
// type for relationship parts and the Main Document part.
func (f *File) setContentTypePartImageExtensions() {
imageTypes := map[string]string{"jpeg": "image/", "png": "image/", "gif": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-", "emz": "image/x-", "wmz": "image/x-"}
imageTypes := map[string]string{
"jpeg": "image/", "png": "image/", "gif": "image/", "svg": "image/", "tiff": "image/",
"emf": "image/x-", "wmf": "image/x-", "emz": "image/x-", "wmz": "image/x-",
}
content := f.contentTypesReader()
content.Lock()
defer content.Unlock()

View File

@ -90,10 +90,12 @@ func TestAddPictureErrors(t *testing.T) {
image.RegisterFormat("wmf", "", decode, decodeConfig)
image.RegisterFormat("emz", "", decode, decodeConfig)
image.RegisterFormat("wmz", "", decode, decodeConfig)
image.RegisterFormat("svg", "", decode, decodeConfig)
assert.NoError(t, f.AddPicture("Sheet1", "Q1", filepath.Join("test", "images", "excel.emf"), ""))
assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), ""))
assert.NoError(t, f.AddPicture("Sheet1", "Q13", filepath.Join("test", "images", "excel.emz"), ""))
assert.NoError(t, f.AddPicture("Sheet1", "Q19", filepath.Join("test", "images", "excel.wmz"), ""))
assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", `{"x_scale": 2.1}`))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx")))
assert.NoError(t, f.Close())
}
@ -175,7 +177,7 @@ func TestGetPicture(t *testing.T) {
func TestAddDrawingPicture(t *testing.T) {
// Test addDrawingPicture with illegal cell reference.
f := NewFile()
assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", "", 0, 0, image.Config{}, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
}
func TestAddPictureFromBytes(t *testing.T) {

View File

@ -72,16 +72,11 @@ type xlsxPhoneticRun struct {
T string `xml:"t"`
}
// commentOptions directly maps the format settings of the comment.
type commentOptions struct {
Author string `json:"author"`
Text string `json:"text"`
}
// Comment directly maps the comment information.
type Comment struct {
Author string `json:"author"`
AuthorID int `json:"author_id"`
Ref string `json:"ref"`
Text string `json:"text"`
Cell string `json:"cell"`
Text string `json:"string"`
Runs []RichTextRun `json:"runs"`
}

View File

@ -29,6 +29,7 @@ var (
NameSpaceDrawingML = xml.Attr{Name: xml.Name{Local: "a", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/main"}
NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"}
NameSpaceDrawingMLSpreadSheet = xml.Attr{Name: xml.Name{Local: "xdr", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"}
NameSpaceDrawing2016SVG = xml.Attr{Name: xml.Name{Local: "asvg", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main"}
NameSpaceSpreadSheetX15 = xml.Attr{Name: xml.Name{Local: "x15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"}
NameSpaceSpreadSheetExcel2006Main = xml.Attr{Name: xml.Name{Local: "xne", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/excel/2006/main"}
NameSpaceMacExcel2008Main = xml.Attr{Name: xml.Name{Local: "mx", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/mac/excel/2008/main"}
@ -95,6 +96,7 @@ const (
ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}"
ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}"
ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}"
ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}"
)
// Excel specifications and limits
@ -163,7 +165,11 @@ var IndexedColorMapping = []string{
}
// supportedImageTypes defined supported image types.
var supportedImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff", ".emf": ".emf", ".wmf": ".wmf", ".emz": ".emz", ".wmz": ".wmz"}
var supportedImageTypes = map[string]string{
".emf": ".emf", ".emz": ".emz", ".gif": ".gif", ".jpeg": ".jpeg",
".jpg": ".jpeg", ".png": ".png", ".svg": ".svg", ".tif": ".tiff",
".tiff": ".tiff", ".wmf": ".wmf", ".wmz": ".wmz",
}
// supportedContentTypes defined supported file format types.
var supportedContentTypes = map[string]string{
@ -234,6 +240,7 @@ type xlsxBlip struct {
Embed string `xml:"r:embed,attr"`
Cstate string `xml:"cstate,attr,omitempty"`
R string `xml:"xmlns:r,attr"`
ExtList *xlsxEGOfficeArtExtensionList `xml:"a:extLst"`
}
// xlsxStretch directly maps the stretch element. This element specifies that a
@ -293,6 +300,28 @@ type xlsxNvPicPr struct {
CNvPicPr xlsxCNvPicPr `xml:"xdr:cNvPicPr"`
}
// xlsxCTSVGBlip specifies a graphic element in Scalable Vector Graphics (SVG)
// format.
type xlsxCTSVGBlip struct {
XMLNSaAVG string `xml:"xmlns:asvg,attr"`
Embed string `xml:"r:embed,attr"`
Link string `xml:"r:link,attr,omitempty"`
}
// xlsxCTOfficeArtExtension used for future extensibility and is seen elsewhere
// throughout the drawing area.
type xlsxCTOfficeArtExtension struct {
XMLName xml.Name `xml:"a:ext"`
URI string `xml:"uri,attr"`
SVGBlip xlsxCTSVGBlip `xml:"asvg:svgBlip"`
}
// xlsxEGOfficeArtExtensionList used for future extensibility and is seen
// elsewhere throughout the drawing area.
type xlsxEGOfficeArtExtensionList struct {
Ext []xlsxCTOfficeArtExtension `xml:"ext"`
}
// xlsxBlipFill directly maps the blipFill (Picture Fill). This element
// specifies the kind of picture fill that the picture object has. Because a
// picture has a picture fill already by default, it is possible to have two

View File

@ -83,6 +83,6 @@ type xlsxRPr struct {
// RichTextRun directly maps the settings of the rich text run.
type RichTextRun struct {
Font *Font
Text string
Font *Font `json:"font"`
Text string `json:"text"`
}