package office import ( "archive/zip" "bytes" "image" "image/color" "image/png" "strings" "testing" ) const wNS = `xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ` + `xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ` + `xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" ` + `xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ` + `xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"` // testDocx zips a minimal package: the body XML plus any extra parts func testDocx(t *testing.T, body string, parts map[string]string) []byte { t.Helper() var buf bytes.Buffer zw := zip.NewWriter(&buf) all := map[string]string{ "word/document.xml": `` + body + ``, } for k, v := range parts { all[k] = v } for name, content := range all { f, err := zw.Create(name) if err != nil { t.Fatalf("zip create: %v", err) } if _, err := f.Write([]byte(content)); err != nil { t.Fatalf("zip write: %v", err) } } if err := zw.Close(); err != nil { t.Fatalf("zip close: %v", err) } return buf.Bytes() } func parseTestDocx(t *testing.T, body string, parts map[string]string) *Document { t.Helper() doc, err := ParseDocx(testDocx(t, body, parts)) if err != nil { t.Fatalf("ParseDocx: %v", err) } return doc } func TestOnOff(t *testing.T) { tests := []struct { xml string set, on bool }{ {``, true, true}, {``, true, true}, {``, true, true}, {``, true, false}, {``, true, false}, {``, true, false}, } for _, tc := range tests { n, err := parseXMLTree([]byte(`` + tc.xml + ``)) if err != nil { t.Fatalf("%s: %v", tc.xml, err) } got := onOff(n.first("b")) if got.set != tc.set || got.v != tc.on { t.Errorf("onOff(%s) = %+v, want set=%v on=%v", tc.xml, got, tc.set, tc.on) } } if got := onOff(nil); got.set { t.Errorf("onOff(nil) = %+v, want unset", got) } } func TestFormatListNumber(t *testing.T) { tests := []struct { v int fmt string want string }{ {3, "decimal", "3"}, {1, "lowerLetter", "a"}, {28, "lowerLetter", "bb"}, {2, "upperLetter", "B"}, {4, "lowerRoman", "iv"}, {1994, "upperRoman", "MCMXCIV"}, {7, "decimalZero", "07"}, {12, "decimalZero", "12"}, {5, "bullet", ""}, } for _, tc := range tests { if got := formatListNumber(tc.v, tc.fmt); got != tc.want { t.Errorf("formatListNumber(%d, %s) = %q, want %q", tc.v, tc.fmt, got, tc.want) } } } func TestPictureTurns(t *testing.T) { tests := []struct { rot float64 turns int ok bool }{ {0, 0, true}, {5400000, 1, true}, {10800000, 2, true}, {16200000, 3, true}, {-5400000, 3, true}, {21600000, 0, true}, {5430000, 1, true}, // within a degree {2700000, 0, false}, } for _, tc := range tests { turns, ok := pictureTurns(tc.rot) if ok != tc.ok || (ok && turns != tc.turns) { t.Errorf("pictureTurns(%v) = %d,%v want %d,%v", tc.rot, turns, ok, tc.turns, tc.ok) } } } func TestOrientInsets(t *testing.T) { in := [4]float64{1, 2, 3, 4} // top right bottom left tests := []struct { name string turns int flipH, flipV bool want [4]float64 }{ {"none", 0, false, false, [4]float64{1, 2, 3, 4}}, {"quarter", 1, false, false, [4]float64{4, 1, 2, 3}}, {"half", 2, false, false, [4]float64{3, 4, 1, 2}}, {"three quarters", 3, false, false, [4]float64{2, 3, 4, 1}}, {"mirror", 0, true, false, [4]float64{1, 4, 3, 2}}, {"flip", 0, false, true, [4]float64{3, 2, 1, 4}}, } for _, tc := range tests { if got := orientInsets(in, tc.turns, tc.flipH, tc.flipV); got != tc.want { t.Errorf("%s: orientInsets = %v, want %v", tc.name, got, tc.want) } } } func TestOrientPixels(t *testing.T) { // a 3x2 picture with a red top-left pixel src := image.NewNRGBA(image.Rect(0, 0, 3, 2)) red := color.NRGBA{R: 255, A: 255} src.SetNRGBA(0, 0, red) tests := []struct { name string turns int flipH, flipV bool w, h, rx, ry int }{ {"quarter turn", 1, false, false, 2, 3, 1, 0}, {"half turn", 2, false, false, 3, 2, 2, 1}, {"three quarters", 3, false, false, 2, 3, 0, 2}, {"mirror", 0, true, false, 3, 2, 2, 0}, {"flip", 0, false, true, 3, 2, 0, 1}, } for _, tc := range tests { out := orientPixels(src, tc.turns, tc.flipH, tc.flipV) if out.Bounds().Dx() != tc.w || out.Bounds().Dy() != tc.h { t.Errorf("%s: size %v, want %dx%d", tc.name, out.Bounds().Size(), tc.w, tc.h) continue } if out.NRGBAAt(tc.rx, tc.ry) != red { t.Errorf("%s: red pixel not at (%d,%d)", tc.name, tc.rx, tc.ry) } } } func TestDocxReaderRunAndParagraphRules(t *testing.T) { tests := []struct { name, body string want []string dont []string }{ { name: "explicit off toggle", body: `plain`, want: []string{`>plain

`}, dont: []string{"font-weight:700"}, }, { name: "spacing before and after", body: `x`, want: []string{"padding-top:12pt;margin-bottom:6pt;"}, }, { name: "exact line height", body: `x`, want: []string{`data-lsexact="18pt"`}, }, { name: "a leading space inside the text collapses as usual", body: `one two`, dont: []string{"pre-wrap"}, }, { name: "a leading space at the start is kept", body: ` lead`, want: []string{"white-space:pre-wrap"}, }, { name: "right tab stop with leader", body: `a1`, want: []string{`data-tabs="right:451.3:dot"`, `` + "\t" + `1`}, }, { name: "page break splits the paragraph", body: `beforeafter`, want: []string{`>before

after

`}, }, { name: "non-breaking and soft hyphens", body: `abc`, want: []string{"a\u2011b\u00adc"}, }, } for _, tc := range tests { doc := parseTestDocx(t, tc.body, nil) for _, w := range tc.want { if !strings.Contains(doc.HTML, w) { t.Errorf("%s: want %q in %s", tc.name, w, doc.HTML) } } for _, d := range tc.dont { if strings.Contains(doc.HTML, d) { t.Errorf("%s: did not want %q in %s", tc.name, d, doc.HTML) } } } } func TestDocxReaderFootnotes(t *testing.T) { body := `text` notes := `` + `` + ` the note` doc := parseTestDocx(t, body, map[string]string{"word/footnotes.xml": notes}) if !strings.Contains(doc.HTML, `1`) { t.Errorf("footnote reference not imported: %s", doc.HTML) } if strings.Count(doc.HTML, "` + `` picture := func(rot string) string { return `` + `` + `` + `` } tests := []struct { name, rot, want string }{ {"upright", ``, "width:40pt;height:20pt;object-fit:fill;object-view-box:inset(10% 0% 0% 0%)"}, {"quarter turn", `rot="5400000"`, "width:20pt;height:40pt;object-fit:fill;object-view-box:inset(0% 10% 0% 0%)"}, } for _, tc := range tests { zipData := testDocx(t, picture(tc.rot), map[string]string{ "word/_rels/document.xml.rels": rels, }) // the media part is binary: add it by rebuilding with the bytes doc := parseDocxWithMedia(t, zipData, "word/media/p.png", pngBuf.Bytes()) if !strings.Contains(doc.HTML, tc.want) { t.Errorf("%s: want %q in %.400s", tc.name, tc.want, doc.HTML) } } } // parseDocxWithMedia adds one binary part to a test package and parses it func parseDocxWithMedia(t *testing.T, zipData []byte, name string, data []byte) *Document { t.Helper() zr, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData))) if err != nil { t.Fatalf("zip: %v", err) } var buf bytes.Buffer zw := zip.NewWriter(&buf) for _, f := range zr.File { rc, err := f.Open() if err != nil { t.Fatalf("zip open: %v", err) } w, _ := zw.Create(f.Name) var b bytes.Buffer if _, err := b.ReadFrom(rc); err != nil { t.Fatalf("zip read: %v", err) } rc.Close() w.Write(b.Bytes()) } w, _ := zw.Create(name) w.Write(data) if err := zw.Close(); err != nil { t.Fatalf("zip close: %v", err) } doc, err := ParseDocx(buf.Bytes()) if err != nil { t.Fatalf("ParseDocx: %v", err) } return doc } func TestDocxRichRoundTrip(t *testing.T) { // the editor's own model survives docx -> model unchanged tests := []struct { name, html string }{ {"spacing and indents", `

x

`}, {"exact line height", `

x

`}, {"line spacing", `

x

`}, {"keep with next", `

x

`}, {"tab stops", `

a` + "\t" + `1

`}, {"footnote reference", `

a1

`}, {"page field", `

1

`}, {"shaded paragraph", `

x

`}, {"trailing line break", `

x

`}, {"horizontal rule", `

a


b

`}, {"bordered paragraph", `

x

`}, } for _, tc := range tests { src := &Document{HTML: tc.html, Footnotes: []Footnote{{ID: "1", HTML: "

note

"}}} data, err := BuildDocx(src) if err != nil { t.Fatalf("%s: BuildDocx: %v", tc.name, err) } back, err := ParseDocx(data) if err != nil { t.Fatalf("%s: ParseDocx: %v", tc.name, err) } if !strings.Contains(back.HTML, tc.html) { t.Errorf("%s: want %s, got %s", tc.name, tc.html, back.HTML) } } } func TestDocxWriterEditorBoxes(t *testing.T) { tests := []struct { name, html string want []string }{ { // docs.css: 3px rule, 12px padding, 8pt + 2pt either side name: "blockquote", html: `
quoted
`, want: []string{``, ``}, }, { // a 1px frame with 10px 12px inside: the half point w:space // cannot hold goes to the spacing outside name: "code block", html: `
code
`, want: []string{``, ``, ``, `w:ascii="Consolas"`}, }, { name: "an indent blockquote from the browser keeps no rule", html: `

x

`, want: []string{`H`, want: []string{``, ``}, }, { name: "imported table stays a Word table", html: `
c
`, want: []string{``}, }, { name: "rule", html: `
`, want: []string{``, `w:line="20" w:lineRule="exact"`}, }, } for _, tc := range tests { data, err := BuildDocx(&Document{HTML: tc.html}) if err != nil { t.Fatalf("%s: BuildDocx: %v", tc.name, err) } body := string(zipPart(t, data, "word/document.xml")) for _, w := range tc.want { if !strings.Contains(body, w) { t.Errorf("%s: want %s in %s", tc.name, w, body) } } } // the indent blockquote draws no rule data, _ := BuildDocx(&Document{HTML: tests[2].html}) if body := string(zipPart(t, data, "word/document.xml")); strings.Contains(body, "") { t.Errorf("border:none blockquote got a rule: %s", body) } } func TestDocxPlainHeaderStaysPlain(t *testing.T) { data, err := BuildDocx(&Document{HTML: "

x

", Header: "Top & tail", Footer: "Bottom"}) if err != nil { t.Fatalf("BuildDocx: %v", err) } back, err := ParseDocx(data) if err != nil { t.Fatalf("ParseDocx: %v", err) } if back.Header != "Top & tail" || back.HeaderHTML != "" { t.Errorf("header = %q / %q, want plain text", back.Header, back.HeaderHTML) } if back.Footer != "Bottom" || back.FooterHTML != "" || back.PageNumbers { t.Errorf("footer = %q / %q pn=%v, want plain text", back.Footer, back.FooterHTML, back.PageNumbers) } }