Skip to content

Read Comments and Replies

Document.Comments() returns every comment in the document as a flat slice in comments.xml order, replies included. Threading is stored as separate metadata rather than as nesting, so a reply looks exactly like a top-level comment until you ask.

GetterReturns
ID()The numeric comment ID.
Author(), Initials()The author name, and their initials or "" if unset.
Date()A *time.Time, nil when the comment carries no date.
Text()The plain text, paragraphs joined with newlines.
Paragraphs()The comment’s paragraphs, for reading formatting or runs.
IsReply(), Parent()Whether the comment is a reply, and the comment it replies to.
Replies()Direct replies, in the order Word displays them.
Done()Whether the thread is marked resolved.

Listing threads

doc, err := document.Open("sample.docx")
if err != nil {
    return err
}
defer doc.Close()

for _, c := range doc.Comments() {
    if c.IsReply() {
        continue
    }
    fmt.Printf("%d. %s: %s (resolved: %t)\n", c.ID(), c.Author(), c.Text(), c.Done())
    for _, r := range c.Replies() {
        fmt.Printf("    %d. %s: %s\n", r.ID(), r.Author(), r.Text())
    }
}

Skipping replies in the outer loop is what stops each reply being printed twice, once on its own and once under its root.

HasComments() reports whether the document has a comments part at all, not whether that part holds anything. A document Word has stripped every comment from can still answer true, with Comments() returning an empty slice; a document that never had comments returns false and a nil slice.

Where threading comes from

IsReply(), Parent(), Replies() and Done() all read commentsExtended.xml, which links comments by the w14:paraId of their last paragraph. UniOffice loads that part along with commentsIds.xml when the document is opened, so the threading survives a round trip.

A document produced by a tool that writes only comments.xml has no such links. Every comment then reports IsReply() false, an empty Replies() and Done() false, even where a human reader would see an obvious back-and-forth. There is no way to recover the thread structure in that case, because it was never recorded.

Limitations

Done() returning false does not distinguish “explicitly not resolved” from “no threading entry for this comment”. Both look the same through the API.

Replies() returns direct replies only. Word writes flat threads, so in practice one level is all there is, but recursing costs nothing and is what the example does.

Text() concatenates run text and discards formatting, hyperlinks and images inside the comment. Reach for Paragraphs() and its runs when the formatting matters.

Run the example

The example opens the bundled sample.docx, which is what the add comments example produces: four comments, one of them a resolved thread with two replies. It takes no command-line arguments. printThread does the indenting.

git clone https://github.com/unidoc/unioffice-examples.git
cd unioffice-examples/document/comment-list
go run main.go

If this is your first time using UniOffice, follow the getting started guide to create an API key and set up your development environment.

View the full source
Last updated on