Office documents: the docx service¶
Using the extension¶
Content Script works with existing Office documents through three services:
| Service | Loads | Returns |
|---|---|---|
| docx | Word documents (.docx) |
CSWordDoc |
| xlsx | Excel spreadsheets (.xlsx) |
CSSpreadsheet |
| pptx | PowerPoint presentations (.pptx) |
CSPPDoc |
A script loads a document, reads or changes it in memory, and saves the result to a node, a resource, or a file. Nothing changes in Content Server until you save.
def node = docman.getNode(123456)
def doc = docx.loadWordDoc(node)
doc.replaceFields([CustomerName: 'Acme Ltd'], true, true)
doc.save(node) // adds a new version to the node
To generate a new Word or PDF document from scratch, use the Document Builder service instead. The services on this page edit documents that already exist, such as templates, contracts under review, or reports.
Module Suite 4.1.0 adds a set of Word methods that read document structure and work with tracked changes, comments, bookmarks, and fields. It also adds named regions for Excel and named shapes for PowerPoint. Members marked 4.1.0 in the tables below are new in this release.
flowchart LR
Source["Document node, version, resource or file"] --> Load["docx.loadWordDoc / xlsx.loadSpreadsheet / pptx.loadPresentation"]
Load --> Read["Read structure: paragraphs, revisions, comments, fields"]
Read --> Edit["Edit: fields, tracked changes, comments, shapes, regions"]
Edit --> Save["save(...)"]
Save --> Version["New version of a node"]
Save --> NewDoc["New document in a folder"]
Save --> Temp["CSResource or File"]
Loading documents¶
| Method | Description | Since |
|---|---|---|
docx.loadWordDoc(CSDocument wordDoc) |
Loads a Word document from a document node | 1.0.0 |
docx.loadWordDoc(File file) |
Loads a Word document from a file | 1.0.0 |
docx.loadWordDoc(CSResource resource) |
Loads a Word document from a CSResource, such as the content of a specific version or a temporary resource returned by another service. Returns null if the resource is null |
4.1.0 |
xlsx.loadSpreadsheet(CSDocument xlsDoc) |
Loads a spreadsheet from a document node | 1.0.0 |
xlsx.loadSpreadsheet(File file) |
Loads a spreadsheet from a file | 1.0.0 |
xlsx.createSpreadsheet(String... name) |
Creates a new spreadsheet. Pass worksheet names to name the initial worksheets | 1.0.0 |
pptx.loadPresentation(CSDocument pptDoc) |
Loads a presentation from a document node | 1.0.0 |
pptx.loadPresentation(File file) |
Loads a presentation from a file | 1.0.0 |
The new CSResource overload lets a script open a version other than the current one, or a document that exists only as a resource:
def node = docman.getNode(123456)
def doc = docx.loadWordDoc(node.versions[0].content)
Spreadsheet methods on docx are deprecated
docx.loadSpreadsheet and docx.createSpreadsheet have been deprecated since 1.7.0. Use the same methods on the xlsx service.
The docx service also works on whole documents:
| Method | Description | Since |
|---|---|---|
docx.compare(CSResource before, CSResource after) |
Compares two documents and returns a CSResource with the comparison result |
2.7.0 |
docx.mergeDocuments(boolean newPageAfterSection, boolean keepHeaderFromFirstDocument, File... files) |
Merges Word files into one document | 2.4.0 |
docx.mergeDocuments(boolean newPageAfterSection, boolean keepHeaderFromFirstDocument, CSWordDoc... files) |
Merges loaded Word documents into one document | 3.0.0 |
docx.replaceFields(CSWordDoc wordDoc, Map parameters, boolean shouldProcessHeaderAndFooters, boolean removeFields) |
Service form of CSWordDoc.replaceFields |
3.3.0 |
pptx.mergeDocuments(boolean keepOriginalLookAndFeel, File... files) |
Merges presentations into one presentation | 1.0.0 |
Saving documents¶
CSWordDoc, CSSpreadsheet, and CSPPDoc share the same save methods. The docx, xlsx, and pptx services also offer save(doc, targetNode), save(doc, targetParent, targetName), save(doc, targetFile), and save(doc), with the document as the first parameter, for example docx.save(doc, node).
| Method | Description | Since |
|---|---|---|
save(CSNode node) |
Saves the document as a new version of the node and returns the node | 1.0.0 |
save(CSNode parentNode, String docname) |
Creates a new document in parentNode and returns the CSDocument |
1.0.0 |
save(CSResource file) |
Writes the document to an existing CSResource |
3.3.0 |
save(File file) |
Writes the document to a file | 1.0.0 |
save() |
Writes the document to a temporary file and returns the File |
1.0.0 |
def doc = docx.loadWordDoc(docman.getNode(123456))
doc.save(docman.getNode(123456)) // new version
doc.save(docman.getNode(2000), 'Contract - final.docx') // new document
def res = docman.getTempResource('out', 'docx')
doc.save(res) // temporary resource
Document properties¶
All three document types read and write core, extended, and custom properties, and synchronize custom properties with a category. These members have been available since 1.0.0.
| Method | Description | Since |
|---|---|---|
listProperties(), listCoreProperties(), listExtendedProperties(), listCustomProperties() |
Return the properties as a Map of String names and values | 1.0.0 |
setProperties(Map properties, String dateFormat) |
Updates several properties at once. dateFormat defaults to dd.MM.yyyy |
1.0.0 |
syncFromCategory(CSNode node, String categoryName, String dateFormat) |
Copies the category attributes of node to custom properties. dateFormat defaults to dd/MM/yyyy |
1.0.0 |
syncToCategory(CSNode node, String categoryName) |
Copies custom properties back to the category of node |
1.0.0 |
Core properties are also available as fields: category, contentStatus, contentType, created, creator, description, identifier, keywords, language, lastModifiedBy, modified, revision, subject, title, and version.
def node = docman.getNode(123456)
def doc = docx.loadWordDoc(node)
doc.title = 'Service Agreement'
doc.syncFromCategory(node, 'Contract')
node.addVersion(doc.save())
Word documents¶
Existing members¶
| Method | Description | Since |
|---|---|---|
replaceVariables(Map context) |
Replaces ${..} placeholders with values from context |
1.0.0 |
evaluateTemplate() |
Runs the document through the templating engine and evaluates every ${..} expression |
1.0.0 |
replaceFields(Map parameters, boolean shouldProcessHeaderAndFooters, boolean removeFields) |
Replaces merge fields. Both booleans default to true |
3.3.0 |
addRowsToTable(Integer tableNumber, Integer targetRow, List replacements, Integer copies) |
Clones a table row. See Cloning table rows | 1.0.0 |
removeRowsFromTable(Integer tableNumber, Integer targetRow, Integer rowNum) |
Removes rowNum rows (default 1) starting at targetRow |
1.0.0 |
removeComments() |
Removes every comment. See Removing comments | 3.0.0 |
listComments(boolean withHtmlContext) |
Lists comments, insertions, and deletions | 3.0.0 |
listCommentsWithPageNumbers(boolean withHtmlContext, boolean foViaXSLT) |
Same as listComments, with page numbers. Both booleans default to true |
3.0.0 |
listCustomXmlBinding(), getCustomXmlBinding(String bindingName), updateCustomXmlBinding(String customXmlBinding, xmlBinding) |
Read and update custom XML data-binding files | 2.3.0 |
updateOpenDoPEBindings(xmlBinding, boolean removeControls, boolean removeXML) |
Updates OpenDoPE data bindings from a String or a parsed XML node | 2.3.0 |
updateOpenDoPEBindingsWithComponents(xmlBinding, Map componentsMap, boolean removeControls, boolean removeXML) |
Same, with components | 3.0.0 |
getOpenDopeDataBinding(), getOpenDopeConditionsBinding(), getOpenDopeXPathsBinding(), getOpenDopeComponentsBinding() and their update... counterparts |
Read and update individual OpenDoPE parts | 3.0.0 |
Reading structure: paragraphs, text, and numbering¶
Most 4.1.0 methods identify a paragraph by its index. The index comes from listParagraphs, which walks the document body in traversal order, including table cells. Use the same index with every paragraph-indexed method.
| Method | Description | Since |
|---|---|---|
listParagraphs() |
Returns every paragraph as a Map with index, text, and style (String or null) |
4.1.0 |
listParagraphs(Map options) |
Returns every paragraph, including those in text boxes, with structure details and optional list numbers | 4.1.0 |
toText(Map options) |
Returns the plain text of the body, one paragraph per line | 4.1.0 |
Each entry returned by listParagraphs(options) contains:
| Key | Type | Description |
|---|---|---|
index |
int | Paragraph index, the same as listParagraphs() |
text |
String | Paragraph text. Includes hyperlinks, content controls, and field results, never field codes |
style |
String or null |
Paragraph style |
paraId |
String or null |
Word's w14:paraId |
inTable |
boolean | The paragraph is in a table |
outlineLevel |
int 0-8 or null |
Outline level, from the paragraph or its style |
With numbering: true, each entry also contains:
| Key | Type | Description |
|---|---|---|
numberLabel |
String or null |
The list number as Word displays it, for example 9.1 |
numberingLevel |
int or null |
List level |
numId |
String or null |
Numbering definition id |
isBullet |
boolean | The paragraph is a bullet item |
The options of listParagraphs(options):
| Option | Default | Description |
|---|---|---|
revisions |
'accepted' |
'accepted' returns the text with every tracked change accepted. 'original' returns the text before the tracked changes. 'asIs' returns the same text as listParagraphs() |
numbering |
false |
Computes list numbers |
toText(options) uses the same paragraphs and text, and accepts revisions plus:
| Option | Default | Description |
|---|---|---|
includeNumbering |
false |
Prefixes numbered paragraphs with their list number, for example 9.1 |
paragraphSeparator |
'\n' |
String placed between paragraphs |
An unrecognised option value raises InvalidParamException.
def doc = docx.loadWordDoc(docman.getNode(123456))
def clauses = doc.listParagraphs([numbering: true])
def clause = clauses.find { it.numberLabel == '9.1' }
log.info("Clause 9.1 is paragraph ${clause?.index}: ${clause?.text}")
def before = doc.toText([revisions: 'original', includeNumbering: true])
def after = doc.toText([revisions: 'accepted', includeNumbering: true])
Which text to use
listParagraphs() without options returns the text as it is stored. listParagraphs(options) defaults to the accepted text. Character offsets used by listRevisions and replaceText refer to the accepted text, so read paragraphs with listParagraphs([:]) or listParagraphs([revisions: 'accepted']) when you compute offsets.
Bookmarks and fields¶
| Method | Description | Since |
|---|---|---|
listBookmarks() |
Returns the bookmarks of the body | 4.1.0 |
listFields() |
Returns the fields of the body in the order they start: cross-references, page numbers, merge fields, hyperlinks, and others | 4.1.0 |
Each bookmark is a Map with name, id (long), hidden (boolean, true for Word's internal bookmarks such as _Ref or _Toc), startParagraphIndex and endParagraphIndex (int or null), and text.
Each field is a Map with:
| Key | Description |
|---|---|
type |
Field type, for example REF, PAGEREF, or MERGEFIELD |
instruction |
The full field code |
result |
The displayed text |
target |
The bookmark of a REF, PAGEREF, or NOTEREF field, or the address of a HYPERLINK. Otherwise null |
paragraphIndex, endParagraphIndex |
Paragraphs where the field starts and ends |
simple |
true for a w:fldSimple field |
deleted |
true if the field is inside a tracked deletion |
Combine the two lists to resolve a cross-reference to the paragraph it points to:
def doc = docx.loadWordDoc(docman.getNode(123456))
def bookmarks = doc.listBookmarks().collectEntries { [(it.name): it] }
doc.listFields().findAll { it.type == 'REF' && !it.deleted }.each { f ->
def target = bookmarks[f.target]
log.info("'${f.result}' in paragraph ${f.paragraphIndex} points to paragraph ${target?.startParagraphIndex}")
}
Merge fields and MERGEFIELD formatting¶
replaceFields(parameters, shouldProcessHeaderAndFooters, removeFields) fills MERGEFIELD fields with the values in parameters.
shouldProcessHeaderAndFooters(defaulttrue) also fills fields in headers and footers.removeFields(defaulttrue) replaces each field with plain text. Withfalse, the fields stay in the document and show the new value as their result.- A field that has no entry in
parametersis filled with!!FieldName!!, which makes missing values easy to spot.
When the fields are kept (removeFields: false), the merged text keeps the formatting of the field in the template, such as its colour. Only MERGEFIELD results are affected. Other fields such as PAGE, TOC, or DOCPROPERTY are left as they are. This step is best effort: if it fails, a warning is logged and the merge result is kept.
def doc = docx.loadWordDoc(docman.getNode(123456))
def names = doc.listFields().findAll { it.type == 'MERGEFIELD' }*.instruction
log.info("Merge fields: ${names}")
doc.replaceFields([CustomerName: 'Acme Ltd', ContractDate: '27.10.2026'], true, false)
Tracked changes¶
Listing revisions¶
| Method | Description | Since |
|---|---|---|
enableTrackChanges(boolean enabled) |
Turns track changes on or off for the document. When on, later edits made in Word are tracked. enabled defaults to true |
4.1.0 |
listRevisions() |
Returns the tracked changes of the body in document order | 4.1.0 |
Each revision is a Map with:
| Key | Description |
|---|---|
id |
long. Use it with the ids filter of acceptRevisions and rejectRevisions |
type |
insertion, deletion, moveFrom, moveTo, paragraphInsertion, paragraphDeletion, runFormatting, or paragraphFormatting |
author |
String |
date |
Date or null |
paragraphIndex |
int, as in listParagraphs |
text |
The text the revision affects |
originalStart, originalEnd |
Offsets of that text in the paragraph's 'original' text |
acceptedStart, acceptedEnd |
Offsets of that text in the paragraph's 'accepted' text |
A deletion is empty in the accepted text, and an insertion is empty in the original text. paragraphInsertion and paragraphDeletion mean that a paragraph mark was inserted or deleted, so paragraphs were split or joined.
Revisions that are not listed
Table row and cell, section, and numbering revisions are not listed, nor are revisions in headers, footers, footnotes, and comments. acceptRevisions and rejectRevisions leave them untouched.
Accepting and rejecting¶
| Method | Description | Since |
|---|---|---|
acceptRevisions(Map filter) |
Accepts tracked changes, as Word's Accept command does | 4.1.0 |
rejectRevisions(Map filter) |
Rejects tracked changes, as Word's Reject command does | 4.1.0 |
Accepting an insertion keeps its text, accepting a deletion removes it, and accepting a formatting change keeps the new formatting. Accepting a deleted paragraph mark joins the paragraph with the next one. Rejecting does the opposite: an insertion is removed, a deletion is restored, the previous formatting comes back, and rejecting an inserted paragraph mark joins the paragraph with the next one.
Without a filter, or with an empty one, every change is accepted or rejected. Filter keys are combined, so a change must match all of them:
| Key | Description |
|---|---|
authors |
List of String. Only changes by these authors (case-insensitive) |
excludeAuthors |
List of String. Never changes by these authors (case-insensitive) |
ids |
List of revision ids from listRevisions |
types |
List of revision types from listRevisions |
def doc = docx.loadWordDoc(docman.getNode(123456))
doc.acceptRevisions([types: ['runFormatting', 'paragraphFormatting']])
.rejectRevisions([authors: ['External Counsel']])
Word-level editing¶
These methods change part of a paragraph or a whole paragraph, optionally as tracked changes. They return the ids of the tracked changes they created, as listRevisions() reports them, or an empty list when the edit is not tracked.
| Method | Description | Since |
|---|---|---|
replaceText(int paragraphIndex, int start, int end, String text, Map options) |
Replaces part of a paragraph's text. Inserts text when start equals end, deletes it when text is empty |
4.1.0 |
insertParagraph(int paragraphIndex, String text, Map options) |
Inserts a new paragraph after, or before, an existing one | 4.1.0 |
deleteParagraph(int paragraphIndex, Map options) |
Deletes a paragraph | 4.1.0 |
All three accept these options:
| Option | Default | Description |
|---|---|---|
track |
Whether track changes is enabled on the document | Records the edit as tracked changes |
author |
'System' |
Author of the tracked changes |
date |
Now | Date of the tracked changes |
insertParagraph also accepts:
| Option | Default | Description |
|---|---|---|
before |
false |
Inserts before the paragraph instead of after it |
likeParagraph |
paragraphIndex |
The paragraph whose properties (style, numbering, indentation) and text formatting are copied |
Details:
startandendinreplaceTextare offsets in the paragraph's accepted text, as returned bylistParagraphs(options)andlistRevisions().- With tracking, the replaced text becomes a tracked deletion and the new text a tracked insertion. The new text takes the formatting of the first replaced run. For a pure insertion, it takes the formatting of the run before the position.
insertParagraphcopies the properties of a reference paragraph, so a new clause is numbered like its neighbours.deleteParagraphwith tracking turns the text and paragraph mark into a tracked deletion. Without tracking, the paragraph is removed.- New text cannot contain line breaks. Tabs are allowed.
What cannot be edited
Text inside an existing tracked insertion, a field, or a content control cannot be edited. A paragraph that contains such text, or that ends a section, cannot be deleted. In these cases, and when a paragraph or offset does not exist, the methods raise InvalidParamException.
Re-read paragraphs after structural edits
Inserting a paragraph, or deleting one without tracking, changes the indexes of the paragraphs that follow. Call listParagraphs again before you use stored indexes.
def doc = docx.loadWordDoc(docman.getNode(123456))
def para = doc.listParagraphs([:]).find { it.text.contains('thirty (30) days') }
int start = para.text.indexOf('thirty (30)')
int end = start + 'thirty (30)'.length()
def ids = doc.replaceText(para.index, start, end, 'sixty (60)',
[track: true, author: 'Legal Review'])
log.info("Created revisions ${ids}")
Paragraph-level helpers¶
Two simpler methods follow the track-changes setting of the document:
| Method | Description | Since |
|---|---|---|
replaceParagraphText(int paragraphIndex, String newText, String author) |
Replaces the whole text of a paragraph. Keeps the paragraph properties and the formatting of the first run. When track changes is enabled, the replacement is recorded as a deletion and an insertion. author defaults to 'System' |
4.1.0 |
addParagraph(String text, int position, String style, String author) |
Adds a paragraph to the body. position is an index in the body content list, not a paragraph index, and -1 (default) appends at the end. style is a Word style id such as Normal, Heading1, or Heading2 (default Normal). When track changes is enabled, the paragraph is a tracked insertion |
4.1.0 |
Comments and comment anchors¶
| Method | Description | Since |
|---|---|---|
listCommentAnchors() |
Returns the comments of the document with the text they are attached to | 4.1.0 |
addComment(String searchText, String commentText, String author, String initials, boolean matchAll, boolean fallbackToParagraph) |
Comments a passage found by a case-insensitive search. Returns the number of comments added, 0 if the text is not found |
4.1.0 |
addCommentToParagraph(int fromParagraph, int toParagraph, String commentText, String author, String initials) |
Comments one or more whole paragraphs. toParagraph is inclusive. Use the same value for a single paragraph |
4.1.0 |
addDocumentComment(String commentText, String author, String initials) |
Adds a comment that is not attached to any text. Its reference is placed at the end of the body | 4.1.0 |
removeComments() |
Removes every comment | 3.0.0 |
Each comment anchor is a Map with:
| Key | Description |
|---|---|
id |
long |
author, initials |
String |
date |
Date or null |
text |
The comment text |
startParagraphIndex, endParagraphIndex |
int or null, as in listParagraphs |
anchoredText |
The commented text, with tracked changes accepted. Empty for a comment attached to a single point |
parentId |
long or null. The comment this one replies to |
done |
boolean. The comment is resolved in Word |
Defaults for the add methods: author is 'System', initials is the first two characters of the author, matchAll is false (only the first occurrence), and fallbackToParagraph is true (if the exact text is not found, the first paragraph with a partial match is commented).
def doc = docx.loadWordDoc(docman.getNode(123456))
def open = doc.listCommentAnchors().findAll { !it.done && it.parentId == null }
open.each { c ->
log.info("${c.author} on '${c.anchoredText}': ${c.text}")
}
doc.addComment('limitation of liability', 'Check against the 2026 policy.', 'Legal Review', 'LR', true, false)
Removing comments¶
removeComments() removes the comment markers from the body and also the comment parts of the package, which hold the comment text and authors. The comments are gone from the saved file, not only hidden. Tracked changes are not affected. To remove them, accept or reject them first.
Removing personal information¶
| Method | Description | Since |
|---|---|---|
removePersonalInformation(Map options) |
Removes information that identifies people or editing sessions before the document leaves the organisation | 4.1.0 |
Text, comments, and tracked changes are kept. Combine this method with removeComments and acceptRevisions for a clean copy.
| Option | Default | Description |
|---|---|---|
coreProperties |
true |
Clears author and last-modified-by |
appProperties |
true |
Clears company and manager |
customProperties |
false |
Removes all custom document properties |
customXml |
false |
Removes custom XML parts. Breaks content controls bound to them |
people |
true |
Removes the list of reviewers Word keeps for comments |
rsids |
true |
Removes editing-session identifiers |
authorAlias |
none | Name that replaces the author of every tracked change and comment, and the document author |
removeOnSave |
false |
Asks Word to remove personal information whenever the document is saved |
Cloning table rows¶
addRowsToTable(tableNumber, targetRow, replacements, copies) copies a template row and fills placeholders in the copies. tableNumber and targetRow are 0-based positions. The copies are inserted directly below targetRow.
copies(default 1) is the number of copies. Each copy is independent of the others, so filling one row does not change another, even in large tables.replacements(default empty) is a List of Maps, one per row, starting with the template row itself. In each Map, every key is literal text to find in the row and its value is the text that replaces it.- Where a replacement is made, the paragraph text is rewritten in its first run, so the paragraph takes the formatting of that run.
- A failure raises
ExecutionFaultException.
def doc = docx.loadWordDoc(docman.getNode(123456))
def items = [
[ITEM: 'Licence', QTY: '10', PRICE: '1,000.00'],
[ITEM: 'Support', QTY: '1', PRICE: '250.00'],
[ITEM: 'Training', QTY: '2', PRICE: '600.00']
]
// Table 0, row 1 holds the placeholders ITEM, QTY and PRICE.
// Two copies plus the template row give three filled rows.
doc.addRowsToTable(0, 1, items, items.size() - 1)
Spreadsheets: named regions¶
| Method | Description | Since |
|---|---|---|
createNamedRegion(String name, String formula) |
Creates a named region | 1.0.0 |
readNamedRegionRange(String name) |
Returns the range of a named region, for example Sheet1!$A$1:$C$10, or null |
4.0.0 |
readNamedRegionValue(String name, int rowIndex, int colIndex) |
Returns the value at a 1-based position in a named region, or null |
4.0.0 |
writeNamedRegion(String name, Object value) |
Writes a value to the top-left cell of a named region | 4.1.0 |
writeNamedRegion is the Excel equivalent of filling a merge field by name. Names are those in Formulas > Name Manager in Excel. Numbers, dates, and booleans are written as such, null clears the cell, and any other value is written as text. An unknown name is logged and skipped, so one map of values can be applied to several templates. Running it twice on the same file gives the same result.
def node = docman.getNode(234567)
def sheet = xlsx.loadSpreadsheet(node)
[CustomerName: 'Acme Ltd', Total: 1250.5, InvoiceDate: new Date()].each { name, value ->
sheet.writeNamedRegion(name, value)
}
sheet.save(node)
The existing spreadsheet API is unchanged: cell reading (readCell, readCellAsInteger, readCellAsDouble, readCellAsDate, readCellAsBoolean), worksheets (getAllWorksheets, getWorksheet, getWorksheetByName, addWorksheet), rows (addRows, newRow), data validation (add...Constraint), formulas (updateFormulas, updateCellsFormulas, forceUpdateFormulas), and replaceVariables.
Presentations: named shapes¶
| Method | Description | Since |
|---|---|---|
replaceVariables(Map context, Integer slideIndex) |
Replaces ${..} placeholders, on one slide or on all slides (-1) |
1.0.0 |
replaceSingleChar(String oldChar, String expression, Integer slide) |
Replaces a single-character placeholder | 1.0.0 |
duplicateSlide(int SLIDE_INDEX, int INSERT_AT, int times) |
Duplicates a slide | 1.0.0 |
updateNamedShapes(Map values, boolean includeContent, boolean includeLayouts, boolean includeMaster) |
Replaces the full text of each text shape whose name is a key in values |
4.1.0 |
setNamedShapesVisible(Map visibility, boolean includeContent, boolean includeLayouts, boolean includeMaster) |
Shows (true) or hides (false) each shape whose name is a key in visibility |
4.1.0 |
Shape names are the names in PowerPoint's Selection Pane. By default only content slides are scanned: includeContent is true, includeLayouts and includeMaster are false. Set them to also update slide layouts or slide masters.
updateNamedShapes replaces the whole text of a shape, so it is safe to run several times on the same file. Hidden shapes stay in the file, and a later run can show them again.
def node = docman.getNode(345678)
def deck = pptx.loadPresentation(node)
deck.updateNamedShapes([Title: 'Q3 Results', Footer: 'Confidential'], true, false, true)
.setNamedShapesVisible([DraftBanner: false], true, false, false)
deck.save(node)
Complete examples¶
Prepare a reviewed contract for signature¶
The script below reads the review state of a contract, accepts the internal changes, rejects one external change set, updates a clause as a tracked change, comments the change, and saves a clean copy for the counterparty.
sequenceDiagram
participant Script
participant Doc as CSWordDoc
participant CS as Content Server
Script->>CS: docman.getNode(id)
Script->>Doc: docx.loadWordDoc(node)
Script->>Doc: listRevisions() and listCommentAnchors()
Script->>Doc: acceptRevisions(filter) and rejectRevisions(filter)
Script->>Doc: listParagraphs(options)
Script->>Doc: replaceText(index, start, end, text, options)
Script->>Doc: addCommentToParagraph(...)
Script->>CS: save(node) as a new version
Script->>Doc: removeComments() and removePersonalInformation(options)
Script->>CS: save(folder, name) as a new document
def node = docman.getNode(123456)
def doc = docx.loadWordDoc(node)
// 1. Review state
def revisions = doc.listRevisions()
def openComments = doc.listCommentAnchors().findAll { !it.done }
log.info("${revisions.size()} tracked changes, ${openComments.size()} open comments")
// 2. Accept the internal team's changes, reject the counterparty's deletions
doc.acceptRevisions([authors: ['Anna Weber', 'Marco Rossi']])
doc.rejectRevisions([authors: ['External Counsel'], types: ['deletion']])
// 3. Change the payment term in clause 9.1 as a tracked change
def clause = doc.listParagraphs([numbering: true]).find { it.numberLabel == '9.1' }
if (clause) {
int start = clause.text.indexOf('30 days')
if (start >= 0) {
doc.replaceText(clause.index, start, start + '30 days'.length(), '60 days',
[track: true, author: 'Legal Review'])
doc.addCommentToParagraph(clause.index, clause.index,
'Payment term aligned with the framework agreement.', 'Legal Review', 'LR')
}
}
// 4. Keep the reviewed document as a new version
doc.save(node)
// 5. Produce a clean copy for the counterparty
doc.acceptRevisions([:])
.removeComments()
.removePersonalInformation([authorAlias: 'Acme Legal'])
doc.save(node.parent, 'Contract - for signature.docx')
Compare two versions as text¶
def node = docman.getNode(123456)
def versions = node.versions
def older = docx.loadWordDoc(versions[-2].content)
def newer = docx.loadWordDoc(versions[-1].content)
def opts = [revisions: 'accepted', includeNumbering: true]
def oldLines = older.toText(opts).readLines()
def newLines = newer.toText(opts).readLines()
def added = newLines - oldLines
def removed = oldLines - newLines
log.info("Added: ${added.size()}, removed: ${removed.size()}")
Fill a Word template, a spreadsheet, and a deck from the same values¶
def values = [CustomerName: 'Acme Ltd', ContractDate: '27.10.2026', Total: '1,250.50']
def folder = docman.getNode(2000)
def word = docx.loadWordDoc(docman.getNode(111111))
word.replaceFields(values, true, true)
word.save(folder, 'Acme - contract.docx')
def sheet = xlsx.loadSpreadsheet(docman.getNode(222222))
values.each { name, value -> sheet.writeNamedRegion(name, value) } // unknown names are skipped
sheet.save(folder, 'Acme - pricing.xlsx')
def deck = pptx.loadPresentation(docman.getNode(333333))
deck.updateNamedShapes(values, true, false, false)
deck.save(folder, 'Acme - proposal.pptx')
Related pages¶
- Document Builder service: generate new Word and PDF documents from a Groovy builder.
- Agents: agent tools that open attached Word files with
docx.loadWordDocand read revisions and comment anchors. - Content Management Object: nodes, versions, and resources used to load and save documents.