Sapling SAP
Reference

Word out and in

The SAP as a Word file for review, and what comes back from Word.

This page is the repository's own document docs/word.md, shown as it is, so that it never drifts from the product.

Statisticians and medical writers work in Microsoft Word. Sapling can write the SAP as a Word file, and it can read a Word file back. The plan (sap.qmd) stays the source. Word is a view: a person reads it, comments on it, and changes the prose in it.

The code is src/docx.ts. It uses no package: a .docx is a ZIP file of XML, and the module reads and writes both.

TaskCommandVerdict
the SAP as a Word filesapling export docxwrote out/sap/sap.docx: n paragraphs, m tables
see what a person changed in that filesapling import docx sap.docxSAPLING DOCX PASS: n prose change(s) ..., m change(s) of blocks for the statistician
write the prose changes into the plansapling import docx sap.docx --applythe same line; sap.qmd has the edits
a Word SAP from elsewhere as a new plansapling import docx other-sap.docx --out plan.qmdSAPLING IMPORT PASS: ...

The plan and its Word file#

The custom property SaplingPlan of the Word file names the .qmd. Back from Word, a changed prose paragraph, heading or list item goes into sap.qmd as Markdown:

Word out: what is in the file#

The file has the parts of the plan in the order of the plan.

BlockIn Word
population, data subset, grouping, derivation, statistics, testone sentence: the id, the name, the condition or the rule
table (tlf)a caption, the population and the columns, a table of the rows (row, variable, statistics, comparison), one line for each model
estimanda caption and a table of its attributes
design, sample size, multiplicity, missing data, sensitivity, interim, software, dictionary, changea caption and a table of its fields
open decisiona yellow paragraph with a red line that starts with OPEN DECISION
decided decisionone sentence with the person, the date and the answer

Below a block is its source: the section of the protocol and the quoted sentence.

How to read the file:

The same plan and the same date give the same file, byte for byte.

The marker#

Each paragraph that Sapling writes has a hidden bookmark. Its name is _sap_<type>_<hash>_<n>.

Word does not show a bookmark whose name starts with _. Word keeps the bookmark when a person edits the paragraph.

When the file comes back, Sapling does this for each paragraph:

  1. It makes the hash of the text now. If the hash is the hash in the marker, Word did not change the paragraph.
  2. If not, Word changed it. Sapling makes the markers of the plan as it is now. The paragraph of the plan with the same marker is the source of the Word paragraph.
  3. If the plan has no such marker, the plan also changed there after the export. This is a conflict. Sapling does not apply it.

There is no other state. You can export, change the plan, and import an old file: Sapling applies what is safe and reports the rest.

Word in: a file that Sapling wrote#

sapling import docx sap.docx prints each change with the line of the plan, the old text and the new text. It does not write the plan. With --apply it writes the prose changes into the plan (sap.qmd).

What changed in WordWhat Sapling does
the text of a prose paragraph, a heading or a list itemapplied: the smallest text edit at that place (src/edit.ts). The rest of the file stays the same, byte for byte: comments, line breaks, indentation.
words in italic or bold (*...*, **...**)applied, when the marks hold only words
a new paragraph after a prose paragraph or a headingapplied: a new paragraph in the plan at that place
the text of a structured block (its sentence or a cell of its table)not applied. Reported for the statistician with the block and its line. The statistician changes the block in the plan, and the compiler checks it.
the cover, the contents, the list of tables, the list of open decisionsnot applied. Sapling makes them again from the plan.
the text of a command in the prose (a grey field)not applied. The report says which command.
an equation, a nested list, other Markdown that the import does not mapnot applied. Change it in the plan.
a paragraph that changed in Word and in the plannot applied: a conflict. Export again, or write the change in the plan.
a paragraph removed in Wordnot applied. Word never removes text from the plan.
two paragraphs joined in Wordnot applied. Join them in the plan.
a new heading, a new list item, a new paragraph after a block or in a tablenot applied. Write it in the plan.

Changes of blanks only are not changes.

After --apply, run sapling check. The edits change prose only, but a sentence can be the words of a citation.

What Sapling never takes from Word#

Comments and tracked changes#

Word in: a Word SAP from elsewhere#

A Word file without markers of Sapling is a SAP from elsewhere (for example a SAP in the TransCelerate template). sapling import docx other-sap.docx --out plan.qmd makes a new plan from it (without --out: sap-from-word.qmd in the study).

Sapling makes no population, method, analysis, output or estimand from the words. Each of them is the statistician's (AGENTS.md, rule 5). Where a heading says that its section holds such statements (analysis sets, estimands, sample size, multiplicity, missing data, interim analyses, efficacy and safety analyses, lists of tables), Sapling writes an open decision div after the section. The decision names the block to write. Two more open decisions at the top ask for the study, the protocol, the ADaM specification, the treatment groups, the visits and the significance level.

The new plan has the status DRAFT, no approval and no decided decision. Approval or signature text in the Word file stays text, and a note says so. The command prints the number of headings, paragraphs and tables it read, and the number of open decisions it wrote.

Limits#

For programs#

src/docx.ts exports:

FunctionResult
zip(files), unzip(buffer)a ZIP file from files; the files of a ZIP file
parseXml(text, strict?)the tree of an XML text
sapDocx(path, text, options?)the .docx of a plan (a Buffer)
exportDocx(path, text, options?)the same, with the numbers of paragraphs, tables, headings and markers
readDocx(buffer)the headings, the paragraphs (text, runs, style, markers, comments) and the tables of a .docx
docxChanges(planText, docx, path?)the changes, the prose edits, and the plan text with the edits (it does not write a file)
docxChangesText(changes, planName?)the report as lines
planFromDocx(docx, options?)the plan skeleton (Markdown) of a Word SAP from elsewhere, with its counts and notes
wordToMd(text)text of Word as text of Markdown, each special character escaped

The functions take a .qmd plan by its path: exportDocx('sap.qmd', text), docxChanges(text, docx, 'sap.qmd').

Quarto's own Word output#

quarto render sap.qmd --to docx with the extension of Sapling also makes a Word file (tried on pilot.qmd, Quarto 1.11.9). It is not the Word export of Sapling, and it does not replace it.

Quarto's Word filesapling export docx
headings, numbers, contentsyes (a TOC field)yes
equationsreal Word equationstext (Greek letters, signs)
footnotes, citationsWord footnotes with the protocol's sentence(Note: ...) in the text; a Source line under a block
blocksa bold title and a table of the attributes (Quarto refuses a callout in a callout in Word; the extension writes no callout for Word)the sentence or a table of the block, the source
cover, list of tables, open decisionsno (the title, subtitle and date only)yes, with the status and the approvals
markersnoa hidden bookmark on every paragraph
the way backnone: sapling import docx reads it as a Word SAP from elsewherethe changed prose into the plan, the rest reported
custom propertiesthe front matter (sapling, sapling-cites, ...)SaplingPlan, SaplingPlanHash, ...

The feature that matters, the tracked round trip of prose changes, needs the markers. Quarto's writer has no place for them (a bookmark on every paragraph would need a filter that writes raw OpenXML for every paragraph), so the hand-written exporter stays.

The tests are in test/docx.test.ts: node --test test/docx.test.ts.