From 00b55caff80a0b58b7609ca072acb3aa5fd3a9c9 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Wed, 7 Oct 2026 18:45:16 +0100 Subject: [PATCH] fix(docs): pair comment ids with the document order, not the spec order (#92) --- docx_comment.go | 10 ++++------ docx_comment_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/docx_comment.go b/docx_comment.go index c50a8a8..fff06cf 100644 --- a/docx_comment.go +++ b/docx_comment.go @@ -65,6 +65,7 @@ func InjectDocxComments(src []byte, author string, comments []DocxComment) ([]by } used := make([]bool, len(comments)) + applied := make([]DocxComment, 0, len(comments)) id := 0 newDoc := docxParaRe.ReplaceAllStringFunc(string(doc), func(p string) string { txt := docxPlain(p) @@ -73,6 +74,9 @@ func InjectDocxComments(src []byte, author string, comments []DocxComment) ([]by continue } used[i] = true + // Record in document order so comments.xml id N matches the marker + // with id N (the spec order may differ from the document order). + applied = append(applied, c) p = docxAddMarkers(p, id) id++ break @@ -80,12 +84,6 @@ func InjectDocxComments(src []byte, author string, comments []DocxComment) ([]by return p }) - applied := make([]DocxComment, 0, len(comments)) - for i, c := range comments { - if used[i] { - applied = append(applied, c) - } - } parts["word/document.xml"] = []byte(newDoc) parts["word/comments.xml"] = docxBuildComments(author, applied) parts["[Content_Types].xml"] = docxAddContentType(parts["[Content_Types].xml"]) diff --git a/docx_comment_test.go b/docx_comment_test.go index 399f3b0..d9d730f 100644 --- a/docx_comment_test.go +++ b/docx_comment_test.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bytes" "io" + "regexp" "strings" "testing" ) @@ -124,6 +125,48 @@ func TestInjectDocxCommentsNoMatch(t *testing.T) { } } +// TestInjectDocxCommentsOrder guards the marker-id ↔ comments.xml pairing when +// the spec order differs from the document order. +func TestInjectDocxCommentsOrder(t *testing.T) { + src := buildTestDocx(t) + spec := []DocxComment{ + {Anchor: "Nothing relevant", Text: "comment B"}, + {Anchor: "The term is two years", Text: "comment A"}, + } + out, applied, err := InjectDocxComments(src, "A", spec) + if err != nil || applied != 2 { + t.Fatalf("inject: applied=%d err=%v", applied, err) + } + doc := unzipPart(t, out, "word/document.xml") + cmts := unzipPart(t, out, "word/comments.xml") + + // id -> 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 {