package onlyoffice import ( "archive/zip" "bytes" "io" "regexp" "strings" "testing" ) // buildTestDocx returns a minimal valid .docx with two paragraphs. func buildTestDocx(t *testing.T) []byte { t.Helper() const ( contentTypes = `` document = `The term is two years, ending 8 October 2028.Nothing relevant here.` rels = `` ) var buf bytes.Buffer zw := zip.NewWriter(&buf) for name, body := range map[string]string{ "[Content_Types].xml": contentTypes, "word/document.xml": document, "word/_rels/document.xml.rels": rels, } { w, err := zw.Create(name) if err != nil { t.Fatalf("zip create: %v", err) } if _, err := w.Write([]byte(body)); err != nil { t.Fatalf("zip write: %v", err) } } if err := zw.Close(); err != nil { t.Fatalf("zip close: %v", err) } return buf.Bytes() } func unzipPart(t *testing.T, data []byte, name string) string { t.Helper() zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { t.Fatalf("zip reader: %v", err) } for _, f := range zr.File { if f.Name != name { continue } rc, err := f.Open() if err != nil { t.Fatalf("open part: %v", err) } defer rc.Close() b, err := io.ReadAll(rc) if err != nil { t.Fatalf("read part: %v", err) } return string(b) } t.Fatalf("part %s not found", name) return "" } func TestInjectDocxComments(t *testing.T) { src := buildTestDocx(t) comments := []DocxComment{ {Anchor: "8 October 2028", Text: "Change to two years."}, {Anchor: "two years", Text: "second anchor, same paragraph"}, {Anchor: "no such text anywhere", Text: "skipped"}, } // First anchor matches the first paragraph; the second also matches that // paragraph but must be skipped (one comment per paragraph). out, applied, err := InjectDocxComments(src, "Andriy Oblivantsev", comments) if err != nil { t.Fatalf("inject: %v", err) } if applied != 1 { t.Fatalf("applied = %d, want 1", applied) } doc := unzipPart(t, out, "word/document.xml") for _, want := range []string{"commentRangeStart", "commentRangeEnd", "commentReference"} { if !strings.Contains(doc, want) { t.Errorf("document.xml missing %s", want) } } if strings.Contains(doc, ` text from comments.xml byID := map[string]string{} for _, m := range regexp.MustCompile(`(?s)]*>(.*?)`).FindAllStringSubmatch(cmts, -1) { txt := strings.TrimSpace(stripTags(m[2])) byID[m[1]] = txt } // paragraph that starts with the term text must point at "comment A" for _, p := range regexp.MustCompile(`(?s)]*>.*?`).FindAllString(doc, -1) { if !strings.Contains(stripTags(p), "The term is two years") { continue } m := regexp.MustCompile(``).FindStringSubmatch(p) if m == nil { t.Fatal("no commentRangeStart in the term paragraph") } if got := byID[m[1]]; got != "comment A" { t.Fatalf("term paragraph points at id %s = %q, want \"comment A\"", m[1], got) } return } t.Fatal("term paragraph not found") } func stripTags(s string) string { return regexp.MustCompile(`<[^>]*>`).ReplaceAllString(s, "") } func TestParseDocxComments(t *testing.T) { got, err := ParseDocxComments([]byte(`[{"anchor":"a","text":"b"}]`)) if err != nil { t.Fatalf("parse: %v", err) } if len(got) != 1 || got[0].Anchor != "a" || got[0].Text != "b" { t.Fatalf("unexpected: %+v", got) } if _, err := ParseDocxComments([]byte(`{"anchor":"a"}`)); err == nil { t.Fatal("expected error for object input") } }