Bug: PageReference omits the separate field character, so MS Word shows "1" instead of the resolved page number (even after "Update Fields")
Package: docx
Version: 9.6.1 (reproduced also on 9.7.1)
Node: v22.22.2
Description
PageReference builds a composite PAGEREF field as:
begin -> instrText("PAGEREF <id>") -> end
all packed into a single <w:r>. Per the OOXML spec, a composite field
should be:
begin -> instrText -> separate -> <cached result run(s)> -> end
The separate field character (w:fldCharType="separate") is what tells
Word "everything after this point, up to end, is the last-computed cached
result — display this until you recompute the field". Without it, Word does
not have a well-formed field to update in place, and both of these happen in
practice:
- If the user declines "update fields on open" (or
updateFields isn't set),
the field displays nothing — no cached text to fall back on.
- If the user does update fields (either via the "This document contains
fields..." prompt, or Ctrl+A → F9), Word does not reliably resolve the
correct page number — in our testing every PAGEREF field in a 28-entry
table of contents displayed 1.
Notably, LibreOffice does not require separate to compute the field
correctly — it recalculates PAGEREF correctly regardless, which is why
this is easy to miss if you only test with LibreOffice/soffice conversion
and not real Microsoft Word.
By contrast, PageNumber.CURRENT (used for PAGE fields, e.g. in footers)
does insert createSeparate() internally (see the children handling in
Run's constructor, the case PageNumber.CURRENT: branch), and that works
correctly in Word. PageReference doesn't follow the same pattern.
Minimal reproduction
import {
Document,
Packer,
Paragraph,
TextRun,
PageReference,
Bookmark,
PageBreak,
} from "docx";
import * as fs from "fs";
const doc = new Document({
features: { updateFields: true },
sections: [
{
children: [
new Paragraph({
children: [new TextRun("See page: "), new PageReference("target")],
}),
new Paragraph({ children: [new PageBreak()] }),
new Paragraph({ children: [new PageBreak()] }),
new Paragraph({
children: [new Bookmark({ id: "target", children: [new TextRun("Here")] })],
}),
],
},
],
});
Packer.toBuffer(doc).then((buf) => fs.writeFileSync("out.docx", buf));
Open out.docx in Microsoft Word, accept "Update Fields", then also try
Ctrl+A → F9. Expected page number is 3; Word displays 1.
Inspecting word/document.xml shows the malformed single-run field:
<w:r>
<w:fldChar w:fldCharType="begin"/>
<w:instrText xml:space="preserve">PAGEREF target</w:instrText>
<w:fldChar w:fldCharType="end"/>
</w:r>
Expected behavior
<w:r><w:fldChar w:fldCharType="begin"/></w:r>
<w:r><w:instrText xml:space="preserve"> PAGEREF target \h </w:instrText></w:r>
<w:r><w:fldChar w:fldCharType="separate"/></w:r>
<w:r><w:t>1</w:t></w:r>
<w:r><w:fldChar w:fldCharType="end"/></w:r>
(The cached text can be a placeholder like "1" — Word will overwrite it on
update. What matters is the separate marker being present.)
Suggested fix
In PageReference's constructor, follow the same pattern already used for
PageNumber.CURRENT inside Run: insert a createSeparate() field
character between the instruction and end, and (ideally) a cached
TextRun placeholder in between:
class PageReference extends Run {
constructor(bookmarkId: string, options: IRunOptions = {}) {
super({
children: [
createBegin(true),
new PageReferenceFieldInstruction(bookmarkId, options),
createSeparate(),
new TextRun({ text: "1", ...options }),
createEnd(),
],
});
}
}
Workaround used in the meantime
We built the field manually using the library's exported low-level
primitives (Run, XmlComponent, XmlAttributeComponent) to insert the
missing separate character and a cached result run:
class FldCharAttrs extends XmlAttributeComponent {
constructor(type: string) {
super({ type });
this.xmlKeys = { type: "w:fldCharType" };
}
}
class RawFldChar extends XmlComponent {
constructor(type: string) {
super("w:fldChar");
this.root.push(new FldCharAttrs(type));
}
}
class PreserveSpaceAttr extends XmlAttributeComponent {
constructor() {
super({ space: "preserve" });
this.xmlKeys = { space: "xml:space" };
}
}
class RawInstrText extends XmlComponent {
constructor(text: string) {
super("w:instrText");
this.root.push(new PreserveSpaceAttr());
this.root.push(text);
}
}
function pageRefField(bookmarkId: string, runOpts = {}) {
return [
new Run({
children: [new RawFldChar("begin"), new RawInstrText(` PAGEREF ${bookmarkId} \\h `), new RawFldChar("separate")],
}),
new TextRun({ text: "1", ...runOpts }),
new Run({ children: [new RawFldChar("end")] }),
];
}
This produces a schema-valid, Word-correct field and resolves correctly
after Ctrl+A → F9 (or on open, with features.updateFields: true,
subject to Word's usual "fields update before final pagination on first
open" timing quirk, which is a separate Word behavior and not part of this
report).
Happy to open a PR with this fix if useful — thanks again for maintaining
this library!
Bug:
PageReferenceomits theseparatefield character, so MS Word shows "1" instead of the resolved page number (even after "Update Fields")Package:
docxVersion: 9.6.1 (reproduced also on 9.7.1)
Node: v22.22.2
Description
PageReferencebuilds a compositePAGEREFfield as:all packed into a single
<w:r>. Per the OOXML spec, a composite fieldshould be:
The
separatefield character (w:fldCharType="separate") is what tellsWord "everything after this point, up to
end, is the last-computed cachedresult — display this until you recompute the field". Without it, Word does
not have a well-formed field to update in place, and both of these happen in
practice:
updateFieldsisn't set),the field displays nothing — no cached text to fall back on.
fields..." prompt, or
Ctrl+A→F9), Word does not reliably resolve thecorrect page number — in our testing every
PAGEREFfield in a 28-entrytable of contents displayed
1.Notably, LibreOffice does not require
separateto compute the fieldcorrectly — it recalculates
PAGEREFcorrectly regardless, which is whythis is easy to miss if you only test with LibreOffice/soffice conversion
and not real Microsoft Word.
By contrast,
PageNumber.CURRENT(used forPAGEfields, e.g. in footers)does insert
createSeparate()internally (see thechildrenhandling inRun's constructor, thecase PageNumber.CURRENT:branch), and that workscorrectly in Word.
PageReferencedoesn't follow the same pattern.Minimal reproduction
Open
out.docxin Microsoft Word, accept "Update Fields", then also tryCtrl+A→F9. Expected page number is3; Word displays1.Inspecting
word/document.xmlshows the malformed single-run field:Expected behavior
(The cached text can be a placeholder like
"1"— Word will overwrite it onupdate. What matters is the
separatemarker being present.)Suggested fix
In
PageReference's constructor, follow the same pattern already used forPageNumber.CURRENTinsideRun: insert acreateSeparate()fieldcharacter between the instruction and
end, and (ideally) a cachedTextRunplaceholder in between:Workaround used in the meantime
We built the field manually using the library's exported low-level
primitives (
Run,XmlComponent,XmlAttributeComponent) to insert themissing
separatecharacter and a cached result run:This produces a schema-valid, Word-correct field and resolves correctly
after
Ctrl+A→F9(or on open, withfeatures.updateFields: true,subject to Word's usual "fields update before final pagination on first
open" timing quirk, which is a separate Word behavior and not part of this
report).
Happy to open a PR with this fix if useful — thanks again for maintaining
this library!