From 1b53d772ee8ae773b94e46bc85915dc9faf7043f Mon Sep 17 00:00:00 2001 From: Todd Date: Thu, 28 Sep 2017 18:12:22 -0500 Subject: [PATCH] document: support inserting paragraphs within a document --- .../document/edit-document/edit-document.docx | Bin 33665 -> 33682 bytes _examples/document/edit-document/main.go | 9 ++ document/document.go | 124 +++++++++++++----- document/document_test.go | 19 +++ 4 files changed, 119 insertions(+), 33 deletions(-) diff --git a/_examples/document/edit-document/edit-document.docx b/_examples/document/edit-document/edit-document.docx index 35995ac3d8f06003012f23888f767d0f21334ea0..1a528735c3fabd7d3a4c030761bc8f9f7d2a4ca7 100644 GIT binary patch delta 2334 zcmZuyc|4SR7k_3jO=*b6j4)Y~q;aR_TCz6TGDcEKH?DOoWhwjq7!oClr=&#`CYq9n z49P@^Fq5*x*h7~o5%FH@JI4Lo+wFbN^T#>QIp6O&-}CwXacGI~^h7wyhAbqE29W&E zpQ-;Cbr2@R_oYrSEw~`a6si_b`hBdFcy46e?{c4k zv2abB*Y4;Ve{D!PQ;yO)G1DXUgjimYWg88JOuUNnHXI%?aj5#VtmR_U4umUsO-M?CrcTexqgn*BBr_Jds~d0+lK z=-=(FI88~ZWr4nq>kUdR-ENGz-<_5`Md!H350w|$*|s?AG z6sN^(aH~lQO(!F@_QDo?0zN!<&MWt1&_>ENE0-x3?f?j5;Nrh8MLcbhzO_!JH?ieh zqrs4`BTj)^Zg47V>riKCjgh)6QIqA*8{VCc_$=9hTpel>NL;A0+ zupV=vgh$;b@r zZ>tz(rvFydd>qfq8lEq)E}(dUM~_uq7Gv^in?chvg|jd7Y^Bi#MwFF38mEey%WI?i zK+j(%0%b=NU$XlPODE`K_u7hcv=c+_6E*ud8eHqc95$w7+3wuV@tiC-&~Bjm@>WfuDg8@`avnqoKJ75h zDOF>SUeTfPOb=uTTa!pKk!+>#FAqmSs@DGc=BeuN1k~F};Q~tBN}Xy-GU-l-oAw_& z)_+l(tV{afmNyuwaxuD7@i8=?$}=IV5=EE7i!FMc2cP?S%UNTh5upp;H{)=lwK*rz zodwwr6y#Fn*X(*K9D~C;eEKH*PFkr`b(wJUEAcEgy-kXtMF(YY5AQ1=l{&$vjRi6L zl{IHEcd|9!HPHR!R25M}3A}BDmZv=sx{(cg`+THySlirr+{HmI1xrl9 z{(dXnYmga}E@F-DZqyI4xKTe7JiQdvDn5n-ix(7JQs|L1JA|lBl%tIEF79pC>l7_# zdm$^IL9yefAMxacohLh3+2D+-Ce8boZJi)O^#V+$xqQ%d$@btAy=yD!ca2}YlIc$0 z@{43e1A{_iiTI3|?Zf-q>`SQ6i4et(Hr?aMNI%Heh{ZNcbUS>+$TTLjvN8hPa&|q% zy}5QUTPnnC!*!aJc5(U!tMiweD#see-hHkow;{gSev=bF6%?=QG$jWbz8TrF=qAGK zQA^owV=*=I{?pb6jMy~R%=Z@;Lw)CTTD9f4n%T=$J<=6sKC!a)>ZuGPQk16C+x(s* zuLevNcjgRrIx3O+oP*&cQpqH*$jBjTOc3Lf{CqbSmMkzVQo|-O;T=b56B*Lpn!$n3+IOWMl&(5h++Sq`7pkoCttgxfFz6av@@)6NbV2mlu&SnK z#>cPy$aTvLO@l%jJ;OZn(P^W_nHm59R#t4t>yRo2P2*kY6oB|B2EPctW(E!23t*3w%+uK8jx_|E(CBi{34$XvD}O=fwda z)IY!-nxv^HtilRhb<9vg05FaQ07?G3|5%v>#F9XJ*^psyH1PK=CD+$XZA*{m+&-s{tZ)OR-{i{#Joz10& z{AbOO1pvPKA50S36szL6s;BJS=cDiV4b1TXu;~W`06@gQ7WSgLxKjfN|8L_0%iHZ= z`0V}v!_I}$V&w%&&%KuTB*ou2TIA;mY={%P_S{pkvM@ymMKgw(L)J8N7!%5%9e_zg u12kLsAu8lRm4j^J)M1`bWSlYVHuNmc628$7LdKiGAczu=N6bg9O8+l5Qu6!& delta 2422 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 WeAx$*jx>hRAxb0;@!{fn%l`qGEz|}8 diff --git a/_examples/document/edit-document/main.go b/_examples/document/edit-document/main.go index b986af96..5d9e08f9 100644 --- a/_examples/document/edit-document/main.go +++ b/_examples/document/edit-document/main.go @@ -39,6 +39,15 @@ func main() { r.ClearContent() r.AddText("John ") r.AddBreak() + + para := doc.InsertParagraphBefore(p) + para.AddRun().AddText("Mr.") + para.SetStyle("Name") // Name is a default style in this template file + + para = doc.InsertParagraphAfter(p) + para.AddRun().AddText("III") + para.SetStyle("Name") + case "LAST NAME": r.ClearContent() r.AddText("Smith") diff --git a/document/document.go b/document/document.go index 5a69aaa8..b5e875a1 100644 --- a/document/document.go +++ b/document/document.go @@ -484,23 +484,23 @@ func (d *Document) FormFields() []FormField { return ret } -func (doc *Document) onNewRelationship(decMap *zippkg.DecodeMap, target, typ string, files []*zip.File, rel *relationships.Relationship, src zippkg.Target) error { +func (d *Document) onNewRelationship(decMap *zippkg.DecodeMap, target, typ string, files []*zip.File, rel *relationships.Relationship, src zippkg.Target) error { dt := gooxml.DocTypeDocument switch typ { case gooxml.OfficeDocumentType: - doc.x = wml.NewDocument() - decMap.AddTarget(target, doc.x, typ, 0) + d.x = wml.NewDocument() + decMap.AddTarget(target, d.x, typ, 0) // look for the document relationships file as well - decMap.AddTarget(zippkg.RelationsPathFor(target), doc.docRels.X(), typ, 0) + decMap.AddTarget(zippkg.RelationsPathFor(target), d.docRels.X(), typ, 0) rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, 0) case gooxml.CorePropertiesType: - decMap.AddTarget(target, doc.CoreProperties.X(), typ, 0) + decMap.AddTarget(target, d.CoreProperties.X(), typ, 0) rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, 0) case gooxml.ExtendedPropertiesType: - decMap.AddTarget(target, doc.AppProperties.X(), typ, 0) + decMap.AddTarget(target, d.AppProperties.X(), typ, 0) rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, 0) case gooxml.ThumbnailType: @@ -514,7 +514,7 @@ func (doc *Document) onNewRelationship(decMap *zippkg.DecodeMap, target, typ str if err != nil { return fmt.Errorf("error reading thumbnail: %s", err) } - doc.Thumbnail, _, err = image.Decode(rc) + d.Thumbnail, _, err = image.Decode(rc) rc.Close() if err != nil { return fmt.Errorf("error decoding thumbnail: %s", err) @@ -524,55 +524,55 @@ func (doc *Document) onNewRelationship(decMap *zippkg.DecodeMap, target, typ str } case gooxml.SettingsType: - decMap.AddTarget(target, doc.Settings.X(), typ, 0) + decMap.AddTarget(target, d.Settings.X(), typ, 0) rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, 0) case gooxml.NumberingType: - doc.Numbering = NewNumbering() - decMap.AddTarget(target, doc.Numbering.X(), typ, 0) + d.Numbering = NewNumbering() + decMap.AddTarget(target, d.Numbering.X(), typ, 0) rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, 0) case gooxml.StylesType: - doc.Styles.Clear() - decMap.AddTarget(target, doc.Styles.X(), typ, 0) + d.Styles.Clear() + decMap.AddTarget(target, d.Styles.X(), typ, 0) rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, 0) case gooxml.HeaderType: hdr := wml.NewHdr() - decMap.AddTarget(target, hdr, typ, uint32(len(doc.headers))) - doc.headers = append(doc.headers, hdr) - rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, len(doc.headers)) + decMap.AddTarget(target, hdr, typ, uint32(len(d.headers))) + d.headers = append(d.headers, hdr) + rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, len(d.headers)) case gooxml.FooterType: ftr := wml.NewFtr() - decMap.AddTarget(target, ftr, typ, uint32(len(doc.footers))) - doc.footers = append(doc.footers, ftr) - rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, len(doc.footers)) + decMap.AddTarget(target, ftr, typ, uint32(len(d.footers))) + d.footers = append(d.footers, ftr) + rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, len(d.footers)) case gooxml.ThemeType: thm := dml.NewTheme() - decMap.AddTarget(target, thm, typ, uint32(len(doc.themes))) - doc.themes = append(doc.themes, thm) - rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, len(doc.themes)) + decMap.AddTarget(target, thm, typ, uint32(len(d.themes))) + d.themes = append(d.themes, thm) + rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, len(d.themes)) case gooxml.WebSettingsType: - doc.webSettings = wml.NewWebSettings() - decMap.AddTarget(target, doc.webSettings, typ, 0) + d.webSettings = wml.NewWebSettings() + decMap.AddTarget(target, d.webSettings, typ, 0) rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, 0) case gooxml.FontTableType: - doc.fontTable = wml.NewFonts() - decMap.AddTarget(target, doc.fontTable, typ, 0) + d.fontTable = wml.NewFonts() + decMap.AddTarget(target, d.fontTable, typ, 0) rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, 0) case gooxml.EndNotesType: - doc.endNotes = wml.NewEndnotes() - decMap.AddTarget(target, doc.endNotes, typ, 0) + d.endNotes = wml.NewEndnotes() + decMap.AddTarget(target, d.endNotes, typ, 0) rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, 0) case gooxml.FootNotesType: - doc.footNotes = wml.NewFootnotes() - decMap.AddTarget(target, doc.footNotes, typ, 0) + d.footNotes = wml.NewFootnotes() + decMap.AddTarget(target, d.footNotes, typ, 0) rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, 0) case gooxml.ImageType: @@ -581,7 +581,7 @@ func (doc *Document) onNewRelationship(decMap *zippkg.DecodeMap, target, typ str continue } if f.Name == target { - path, err := zippkg.ExtractToDiskTmp(f, doc.TmpPath) + path, err := zippkg.ExtractToDiskTmp(f, d.TmpPath) if err != nil { return err } @@ -589,14 +589,72 @@ func (doc *Document) onNewRelationship(decMap *zippkg.DecodeMap, target, typ str if err != nil { return err } - iref := common.MakeImageRef(img, &doc.DocBase, doc.docRels) - doc.Images = append(doc.Images, iref) + iref := common.MakeImageRef(img, &d.DocBase, d.docRels) + d.Images = append(d.Images, iref) files[i] = nil } } - rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, len(doc.Images)) + rel.TargetAttr = gooxml.RelativeFilename(dt, src.Typ, typ, len(d.Images)) default: log.Printf("unsupported relationship type: %s tgt: %s", typ, target) } return nil } + +// InsertParagraphAfter adds a new empty paragraph after the relativeTo +// paragraph. +func (d *Document) InsertParagraphAfter(relativeTo Paragraph) Paragraph { + return d.insertParagraph(relativeTo, false) +} + +// InsertParagraphBefore adds a new empty paragraph before the relativeTo +// paragraph. +func (d *Document) InsertParagraphBefore(relativeTo Paragraph) Paragraph { + return d.insertParagraph(relativeTo, true) +} + +func (d *Document) insertParagraph(relativeTo Paragraph, before bool) Paragraph { + if d.x.Body == nil { + return d.AddParagraph() + } + + for _, ble := range d.x.Body.EG_BlockLevelElts { + for _, c := range ble.EG_ContentBlockContent { + for i, p := range c.P { + // foudn the paragraph + if p == relativeTo.X() { + p := wml.NewCT_P() + if before { + c.P = append(c.P, nil) + copy(c.P[i+1:], c.P[i:]) + c.P[i] = p + } else { + c.P = append(c.P, nil) + copy(c.P[i+2:], c.P[i+1:]) + c.P[i+1] = p + } + return Paragraph{d, p} + } + } + + if c.Sdt != nil && c.Sdt.SdtContent != nil && c.Sdt.SdtContent.P != nil { + for i, p := range c.Sdt.SdtContent.P { + if p == relativeTo.X() { + p := wml.NewCT_P() + if before { + c.Sdt.SdtContent.P = append(c.Sdt.SdtContent.P, nil) + copy(c.Sdt.SdtContent.P[i+1:], c.Sdt.SdtContent.P[i:]) + c.Sdt.SdtContent.P[i] = p + } else { + c.Sdt.SdtContent.P = append(c.Sdt.SdtContent.P, nil) + copy(c.Sdt.SdtContent.P[i+2:], c.Sdt.SdtContent.P[i+1:]) + c.Sdt.SdtContent.P[i+1] = p + } + return Paragraph{d, p} + } + } + } + } + } + return d.AddParagraph() +} diff --git a/document/document_test.go b/document/document_test.go index a171dfd2..8b6b399b 100644 --- a/document/document_test.go +++ b/document/document_test.go @@ -82,3 +82,22 @@ func TestOpenWord2016(t *testing.T) { } testhelper.CompareGoldenZipFilesOnly(t, "../../testdata/Office2016/Word-Windows.docx", got.Bytes()) } + +func TestInsertParagraph(t *testing.T) { + doc := document.New() + if len(doc.Paragraphs()) != 0 { + t.Errorf("expected 0 paragraphs, got %d", len(doc.Paragraphs())) + } + p := doc.AddParagraph() + before := doc.InsertParagraphBefore(p) + after := doc.InsertParagraphAfter(p) + if len(doc.Paragraphs()) != 3 { + t.Errorf("expected 3 paragraphs, got %d", len(doc.Paragraphs())) + } + if doc.Paragraphs()[0].X() != before.X() { + t.Error("InsertParagraphBefore failed") + } + if doc.Paragraphs()[2].X() != after.X() { + t.Error("InsertParagraphAfter failed") + } +}