Skip to content

Bookmarks

A bookmark names a location in a document so that something else can point at it: a hyperlink, a cross-reference, or a table of contents entry. AddBookmark allocates a fresh numeric ID on every call, and that ID is an internal detail of the OOXML markup. The name is the handle you should hold onto.

Anchoring and linking

intro := doc.AddParagraph()
intro.AddBookmark("intro")
intro.AddRun().AddText("Introductory paragraph, anchored by \"intro\".")

h1 := doc.AddParagraph()
h1.SetStyle("Heading1")
h1.AddBookmark("section1")
h1.AddRun().AddText("Section One")

link := doc.AddParagraph()
hl := link.AddHyperLink()
section1, _ := doc.BookmarkByName("section1")
hl.SetTargetBookmark(section1)
hl.AddRun().AddText("Jump to Section One")

BookmarkByName returns the bookmark and a bool reporting whether the name was found. The example discards the bool because it just added the bookmark; in code that walks a document someone else produced, check it.

Names have to be unique for this to work, and nothing enforces that. AddBookmark will happily write two bookmarks with the same name, and BookmarkByName then returns the first one in document order.

Reading bookmarks back

Document.Bookmarks returns every bookmark start in the document body, in document order. Each one carries the accessors below.

AccessorReturns
Name()The name used as a hyperlink anchor.
ID()The numeric ID shared with the matching end marker.
IsEmpty()Whether the range contains no runs.
Text()The concatenated text of every run in the range.
Runs()The runs inside the range, live and editable.
Paragraphs()The paragraphs the range touches.

A bookmark added by AddBookmark is zero-width: the start and end markers sit next to each other with nothing in between. IsEmpty reports true for those, Text returns an empty string and Runs returns an empty slice. Bookmarks that wrap actual content come from documents you open, where Word wrote a range around a selection.

To edit the text near a zero-width anchor, go through the paragraph instead:

bm, _ := doc.BookmarkByName("summary")
p := bm.Paragraphs()[0]
p.AddRun().AddText(" -- appended via bookmark lookup.")

Limitations

Bookmarks walks the document body only. Bookmarks placed in a header or a footer are not returned, so a lookup for one of those names fails even though the markup is there. Within the body it does descend into table cells and into block-level structured document tags.

Both BookmarkByName and the range accessors are linear walks. BookmarkByName scans the slice from Bookmarks, and Runs, Paragraphs, Text and IsEmpty each walk the body afresh to resolve the range. Cache the slice if you are processing many bookmarks in a large document.

The range walker does not descend into CustomXml, SmartTag, Dir or Bdo wrappers. If a bookmark’s end marker happens to sit inside one of those, the walker never sees it and the resolved range runs on past where it should stop.

Paragraphs is defined in terms of what the range touches, which is not always one paragraph. A bookmark spanning a table returns each cell paragraph the range covers, and a bookmark whose markers sit between block-level elements may return only the paragraphs inside the range rather than the enclosing ones.

Run the example

The example writes bookmarks.docx and prints every bookmark it created to stdout with its name, ID, shape and text, which is the quickest way to see what IsEmpty and Text do for an anchor as opposed to a range.

git clone https://github.com/unidoc/unioffice-examples.git
cd unioffice-examples/document/bookmarks
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