Fragments and document assembly¶
Template syntax expresses content that varies. It expresses structure that varies badly — a findings section that is a heading and a bulleted list for one customer and a table for another is a page of nested tags nobody wants to maintain in Word.
A Fragment is the escape hatch. Build the content with the ordinary
python-docx-ng API and drop it in at a tag:
from docxtpl import Template
tpl = Template("report.docx")
findings = tpl.new_fragment()
findings.document.add_heading("Findings", level=2)
for finding in results:
findings.document.add_paragraph(finding.title, style="List Bullet")
findings.document.add_paragraph(finding.detail)
tpl.render({"findings": findings})
tpl.save("report-acme.docx")
In the template, in a paragraph of its own:
{{p findings }}
Why the p prefix¶
{{p findings }} is scoped to the paragraph: the paragraph is replaced by
the fragment's block-level content. A fragment produces paragraphs and tables,
and those cannot live inside another paragraph.
An unprefixed {{ findings }} works too — the paragraph is divided around the
inserted content — but {{p }} says what you mean and leaves nothing behind.
Lists, bullets and numbering¶
A list item is not formatting on a run — it is a paragraph property, pointing
at a numbering definition in word/numbering.xml. So it is not something a
Text can express, and add_list() is on the fragment:
findings = tpl.new_fragment()
findings.add_list(
["Access control", ["No MFA on the console", "Shared service account"], "Logging"],
numbered=True,
)
tpl.render({"findings": findings})
- Access control a. No MFA on the console b. Shared service account
- Logging
Nesting is the level. A string is an item; a sequence is a sublist one level deeper. OOXML allows nine, and a tenth is an error rather than a silent flatten.
numbered=False — the default — gives bullets. bullets=("–", "·") and
formats=(WD_NUMBER_FORMAT.UPPER_ROMAN,) choose the glyph or the counter per
level, cycling when shorter than the depth. style="List Paragraph" names a
paragraph style for the look; the numbering comes from the call whatever the
style says.
add_list() returns the paragraphs it made, so anything else you want to do to
them is the ordinary API.
Each list starts at 1¶
add_list() defines its own list and the definition travels with the
content. Two consequences, both of which are the point:
- a loop producing one list per item gets a list per item, each starting at 1,
with no
w:startOverrideand no bookkeeping; - the template needs no list in it. Nothing has to be pre-authored in Word
for a definition to point at, and nothing depends on the template having a
List Bulletstyle.
For a numbering scheme this does not cover — cumulative 1.1.1 level text,
locale-specific formats, a list continuing across fragments —
fragment.document.numbering is python-docx-ng's whole numbering model, and
paragraph.set_numbering(num_id, level) puts a paragraph in a list you defined
yourself.
Merging an existing document¶
The body of that file is inserted. Its section definition is not: page size and margins belong to the document being rendered into.
new_fragment() takes a path, anything path-like, an open stream, or a
docx.document.Document — and Fragment.from_file() is the same thing spelled
as a classmethod, for code that already uses it.
Composing from documents another template rendered¶
A document that is already open needs no round trip through a file. Pass it straight in:
section = Template("section.docx")
section.render({"client": client})
report = Template("report.docx")
report.render({"body": report.new_fragment(section.document)})
The document need never have been saved — one built by hand with
docx.Document() works the same way, images, hyperlinks and lists included.
It is also not copied. Content is read out of it when the fragment renders,
so anything added in between arrives, and the document is yours to go on using
afterwards. Fragment.from_document() is the same thing under a name that says
so.
Render the pieces first
A fragment carries content, not template syntax. A {{ }} still sitting in
the source document is inserted as the characters {{ }}, because the
fragment's markup is spliced in after the tags of the outer template have
already been read. Render each piece with its own Template before
inserting it, as above.
What comes with it¶
Assembling documents that were written separately is the fiddliest thing in this package, because the content refers to things by id and the ids mean different things in the two files. Three kinds of reference are carried across:
Relationships — images and hyperlinks. Re-created in the part being rendered, so they point at something. An image identical to one the target already has is not stored twice.
Numbering — the definitions behind list formatting. Always renumbered: two documents that each numbered their lists from 1 would otherwise share a definition, and the merged list would carry on the target's counter, starting at 4 because the target's own list ended at 3.
Styles — target wins. A style the source defines and the target already has keeps the target's definition, so merged content looks like the document it was merged into rather than like the one it came from. A style the target does not have is copied across, because the alternative is content that silently renders as Normal.
Target-wins is a choice, and it can surprise
If both documents define Heading 2 and they differ, the merged headings
look like the target's. That is what you want when assembling a report from
fragments and not what you want when quoting another document verbatim. For
the second case, rename the style in the source before merging.
Documents a converter wrote¶
Pandoc, LibreOffice and every reporting tool's "export to Word" produce a .docx
that Word did not, and they are a large share of what people actually merge.
They work, and the suite says so rather than assuming: tests/test_foreign.py
inserts a LibreOffice-produced document into a Word-produced one — twice over,
which is when ids collide — and checks the content, the table, the image, the
link, the numbering and the schema.
Three things are handled that a naive merge gets wrong, and all three are routine in converter output:
Namespace prefixes. A converter may bind the wordprocessing namespace to any
prefix it likes; <ww:p> and <w:p> are the same element. Content is parsed
rather than concatenated, so the prefix a document happens to use is not part of
what is carried.
Embedded objects. Two documents written separately both call their first
embedded object oleObject1.bin. The second is renamed to oleObject2.bin and
its relationship follows it, so both objects survive. An object identical to
one already there is reused rather than duplicated.
Drawing ids. Two wp:docPr elements claiming one id is among the few things
Word calls unreadable rather than merely wrong. They are renumbered across the
whole part after every merge.
The compatibility name¶
Subdoc is the same object, and tpl.new_subdoc() returns one: