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.
| Task | Command | Verdict |
|---|---|---|
| the SAP as a Word file | sapling export docx | wrote out/sap/sap.docx: n paragraphs, m tables |
| see what a person changed in that file | sapling import docx sap.docx | SAPLING DOCX PASS: n prose change(s) ..., m change(s) of blocks for the statistician |
| write the prose changes into the plan | sapling import docx sap.docx --apply | the same line; sap.qmd has the edits |
| a Word SAP from elsewhere as a new plan | sapling import docx other-sap.docx --out plan.qmd | SAPLING 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:
- Sapling finds the range of the
.qmdthe paragraph came from. The text between the commands of that range (spans, math, code, links, footnotes, comments) is text; a command is kept as it is written.*words*and**words**are words between two marks: their words can change. - Only the changed characters are replaced (the smallest edit). Line breaks, indentation, comments and every other
byte of the
.qmdstay the same. - The text of Word is escaped for Markdown:
\*_[]{}$`~^|<, and<!--. A new paragraph that starts like a list, a heading, a quote or a div is escaped at the start of its line. If a person types[x]{.var}or::: {.decision}in Word, the plan gets the characters, not a span or a div. - As a last check, Sapling compares the nodes of the
.qmdthat are not text (divs, spans, headings, lists, math, with their attributes) before and after the edits. If they are not the same, it applies nothing. - A new paragraph after a prose paragraph goes after it; after a heading, after the line of the heading.
- The cover, an approval, a decision, a block: reported and never applied.
Word out: what is in the file#
The file has the parts of the plan in the order of the plan.
- The cover. The title, the subtitle, the study, the version, the date, the status, the approvals, the treatment groups, the visits and the significance level.
- Contents. A table of contents of Word (a
TOCfield) with the headings of levels 1 to 3. In Word, click it and select Update Field to get the page numbers. - List of tables. One row for each output: its id, its title and its population.
- Open decisions. The number of open decisions, and one row for each: its id, its section and its text.
- The headings. Each heading has its number and the style Heading 1 to Heading 4. The navigation pane of Word shows them.
- The prose. Each paragraph of the plan is one paragraph in the style Normal. A list is a list of Word (bullets or numbers). A line break in the plan file is a space in Word.
- The structured blocks. Each block is its sentence or a small table:
| Block | In Word |
|---|---|
| population, data subset, grouping, derivation, statistics, test | one 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 |
| estimand | a caption and a table of its attributes |
| design, sample size, multiplicity, missing data, sensitivity, interim, software, dictionary, change | a caption and a table of its fields |
| open decision | a yellow paragraph with a red line that starts with OPEN DECISION |
| decided decision | one 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:
- A paragraph with a grey line at its left is a structured block. You cannot change it from Word.
- A yellow paragraph is an open decision. Only a person decides it, in the plan.
- Text with a grey background in the prose is a command of the plan: a citation (
[Protocol §4.1]), a variable, a reference to a table, math. You cannot change it from Word. - All other text in the style Normal, in a heading or in a list is prose. You can change it.
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>.
<type>is one letter:pprose,hheading,llist item,bblock,ccover or generated list,rother text of the plan (an equation, a nested list).<hash>is 12 characters made from the text of the paragraph as Sapling wrote it.<n>counts equal paragraphs. The first one has 0.
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:
- It makes the hash of the text now. If the hash is the hash in the marker, Word did not change the paragraph.
- 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.
- 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 Word | What Sapling does |
|---|---|
| the text of a prose paragraph, a heading or a list item | applied: 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 heading | applied: 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 decisions | not 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 map | not applied. Change it in the plan. |
| a paragraph that changed in Word and in the plan | not applied: a conflict. Export again, or write the change in the plan. |
| a paragraph removed in Word | not applied. Word never removes text from the plan. |
| two paragraphs joined in Word | not applied. Join them in the plan. |
| a new heading, a new list item, a new paragraph after a block or in a table | not 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#
- An approval. The status and the approvals on the cover are made from the plan. A change of them in Word is
reported and not applied. Only a person writes
approved:in the front matter of the plan. - A decision. An open decision stays open. A person decides it in the plan (
sapling decide, oranswer=,by=andon=written by that person). - A structured block. A population, a table, an analysis, a method, an estimand, a derivation.
- Markup. Text from Word is text. Each character that Markdown reads as markup is escaped (the list in
The plan and its Word file). If a person types
approved:or::: {.decision}in Word, the plan gets the characters, not a setting or a div. The divs and spans of the plan in a changed paragraph stay as they are written. - As a last check, Sapling compares the nodes of the plan that are not text before and after the edits. If they are not the same, it applies nothing.
Comments and tracked changes#
- Comments. Sapling lists each comment with its author, its date, its text and the paragraph it is on (the block
and the line of the plan). It does not write comments into the plan. A reviewer who wants a comment in the plan
writes a
<!-- review (Name, date): text -->comment above the block. - Tracked changes. Sapling reads the file as if each tracked change is accepted. Inserted text is read. Deleted text is not read. The report gives the number of insertions and deletions. If you do not want a tracked change, reject it in Word before the import.
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).
- Headings become Markdown headings,
#to####. Sapling knows a heading by its Word style (Heading 1 ... in each language of Word) or its outline level. A number typed before the title is removed: the plan numbers its sections. - Paragraphs stay as text. Bold and italic stay. Each special character is escaped.
- Lists become Markdown lists (
- item,1. item). - Tables become pipe tables of text.
- The title of the Word file becomes
title:in the front matter. The entries of the table of contents are not read. - Text before the first heading goes into a section "Front matter of the Word SAP".
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#
- Math is shown as text: Greek letters and common signs as characters, the rest as the LaTeX math of the plan. There is no Word equation. You cannot change math from Word.
- Footnotes of the plan are shown in the text as
(Note: ...). - The table shells of the appendix are not in the Word file. The PDF (
sapling sap pdf) has them. - Format changes made in Word (bold, italic, colour, styles) are not read back. Only text is.
- A moved paragraph is not moved in the plan. If its text did not change, Sapling sees no change.
- From a Word SAP from elsewhere Sapling does not read: footnotes, headers and footers, text boxes, pictures, merged cells (each cell is read as one cell), fields, comments. A table inside a table cell is read as text of the cell.
- Markdown the import does not map: a change in a paragraph with such Markdown (a link written
[text](url)whose text changed) is reported, not applied. - A Word file without heading styles: a short bold line that starts with a section number (
3.1 Methods) is read as a heading. A note says so. - Checked with LibreOffice (the file opens, converts to PDF, and keeps the markers when LibreOffice saves it) and
macOS
textutil. A check with Microsoft Word itself is still to do.
For programs#
src/docx.ts exports:
| Function | Result |
|---|---|
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 file | sapling export docx | |
|---|---|---|
| headings, numbers, contents | yes (a TOC field) | yes |
| equations | real Word equations | text (Greek letters, signs) |
| footnotes, citations | Word footnotes with the protocol's sentence | (Note: ...) in the text; a Source line under a block |
| blocks | a 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 decisions | no (the title, subtitle and date only) | yes, with the status and the approvals |
| markers | no | a hidden bookmark on every paragraph |
| the way back | none: sapling import docx reads it as a Word SAP from elsewhere | the changed prose into the plan, the rest reported |
| custom properties | the 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.