dsh-office-kit 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CAPABILITY.md +147 -68
- package/README.md +3 -1
- package/assets/skills/excel-live-control/SKILL.md +234 -0
- package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
- package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
- package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
- package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
- package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
- package/assets/skills/excel-live-control/references/office-automation.md +207 -0
- package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
- package/assets/skills/excel-live-control/style_guidelines.md +87 -0
- package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
- package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
- package/assets/skills/office-documents/SKILL.md +90 -77
- package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
- package/assets/skills/office-documents/ooxml/comments.md +40 -0
- package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
- package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
- package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
- package/assets/skills/office-documents/references/docx-structure.md +83 -0
- package/assets/skills/office-documents/references/template-elicitation.md +49 -0
- package/assets/skills/office-documents/references/writing-quality.md +49 -0
- package/assets/skills/office-documents/render_docx.py +99 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +201 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +158 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +155 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +115 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +111 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +109 -0
- package/assets/skills/office-documents/scripts/comments_add.py +143 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +121 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +92 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +81 -0
- package/assets/skills/office-documents/scripts/content_controls.py +199 -0
- package/assets/skills/office-documents/scripts/core_properties.py +131 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +594 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +232 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +106 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +73 -0
- package/assets/skills/office-documents/scripts/fields_report.py +66 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +69 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +84 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +103 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +88 -0
- package/assets/skills/office-documents/scripts/images_audit.py +87 -0
- package/assets/skills/office-documents/scripts/insert_note.py +133 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +97 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +91 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +153 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +176 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +246 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +138 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +120 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +108 -0
- package/assets/skills/office-documents/scripts/replace_text.py +161 -0
- package/assets/skills/office-documents/scripts/section_audit.py +98 -0
- package/assets/skills/office-documents/scripts/set_protection.py +99 -0
- package/assets/skills/office-documents/scripts/style_lint.py +118 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +110 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +174 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +55 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +74 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +124 -0
- package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
- package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
- package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
- package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
- package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
- package/assets/skills/office-documents/tasks/create_edit.md +107 -0
- package/assets/skills/office-documents/tasks/equations.md +59 -0
- package/assets/skills/office-documents/tasks/fields_update.md +44 -0
- package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
- package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
- package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
- package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
- package/assets/skills/office-documents/tasks/images_figures.md +63 -0
- package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
- package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
- package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
- package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
- package/assets/skills/office-documents/tasks/read_review.md +50 -0
- package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
- package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
- package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
- package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
- package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
- package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
- package/assets/skills/office-documents/tasks/verify_render.md +50 -0
- package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
- package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
- package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
- package/assets/skills/office-pdf/SKILL.md +73 -56
- package/assets/skills/office-pdf/references/form-fields.md +56 -0
- package/assets/skills/office-pdf/references/geometry.md +44 -0
- package/assets/skills/office-pdf/references/layout-quality.md +41 -0
- package/assets/skills/office-pdf/references/limits.md +58 -0
- package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
- package/assets/skills/office-pdf/scripts/forms.py +556 -0
- package/assets/skills/office-pdf/scripts/metadata.py +161 -0
- package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
- package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
- package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
- package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
- package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
- package/assets/skills/office-pdf/tasks/annotations.md +98 -0
- package/assets/skills/office-pdf/tasks/create.md +75 -0
- package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
- package/assets/skills/office-pdf/tasks/forms.md +67 -0
- package/assets/skills/office-pdf/tasks/read.md +62 -0
- package/assets/skills/office-pdf/tasks/redact.md +72 -0
- package/assets/skills/office-pdf/tasks/stamp.md +60 -0
- package/assets/skills/office-presentations/SKILL.md +141 -87
- package/assets/skills/office-presentations/container_tools/add_slides.py +143 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +127 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +133 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +292 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +164 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +151 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +206 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +219 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +352 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +382 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +146 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +175 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +236 -0
- package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
- package/assets/skills/office-presentations/recipes/README.md +71 -0
- package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
- package/assets/skills/office-presentations/recipes/connectors.md +83 -0
- package/assets/skills/office-presentations/recipes/images.md +80 -0
- package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
- package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
- package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
- package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
- package/assets/skills/office-presentations/recipes/tables.md +88 -0
- package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
- package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
- package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
- package/assets/skills/office-presentations/references/finalization.md +63 -0
- package/assets/skills/office-presentations/references/native-bullets.md +78 -0
- package/assets/skills/office-presentations/references/native-evidence.md +67 -0
- package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
- package/assets/skills/office-presentations/references/templates.md +76 -0
- package/assets/skills/office-presentations/routing/google_slides.md +45 -0
- package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
- package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
- package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
- package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
- package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
- package/assets/skills/office-presentations/tasks/template-following.md +132 -0
- package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +183 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +161 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +118 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +246 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +192 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +243 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +114 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +129 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +313 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +405 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +287 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +142 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +141 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +140 -0
- package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
- package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
- package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
- package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
- package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
- package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
- package/assets/skills/office-spreadsheets/references/charts.md +63 -0
- package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
- package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
- package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
- package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
- package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
- package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
- package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
- package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
- package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
- package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
- package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
- package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
- package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
- package/assets/skills/shared/annotation-recipes.md +252 -0
- package/assets/skills/shared/check_runtime.py +260 -0
- package/assets/skills/shared/common-workflow.md +139 -0
- package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/index.js +246 -89
- package/package.json +1 -1
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +213 -254
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Footnotes and endnotes
|
|
2
|
+
|
|
3
|
+
**Goal.** Attach a reference note at exactly one place in the text, and keep the note body in the notes part rather than in the body.
|
|
4
|
+
|
|
5
|
+
Notes live outside the flow: `word/footnotes.xml` (or `word/endnotes.xml`) holds the text, and a `w:footnoteReference w:id="N"` run in the body points at it. The part also needs the separator notes (ids -1 and 0) or Word repairs the file.
|
|
6
|
+
|
|
7
|
+
## Insert a note
|
|
8
|
+
|
|
9
|
+
Put a marker where the reference belongs, then convert it:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
"<python>" "<skill>/scripts/insert_note.py" "<file.docx>" --kind footnote --text "Source: ledger extract, June." --out "<file-notes.docx>"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The marker defaults to `[[NOTE]]`; change it with `--marker`. The script creates the notes part (with separator and continuation-separator entries) if it does not exist, allocates the next free id, splits the surrounding run so only the marker is replaced by the reference, and styles the note text with `FootnoteText` and the reference with `FootnoteReference`. `--kind endnote` writes to the endnotes part instead.
|
|
16
|
+
|
|
17
|
+
## Report
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
"<python>" "<skill>/scripts/footnotes_report.py" "<file.docx>"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Lists each note's text, how many references point at it, and which references dangle or which notes are orphaned. Orphans appear when content is copied between documents; dangling references appear when a note is deleted but its marker stays.
|
|
24
|
+
|
|
25
|
+
## Renumbering
|
|
26
|
+
|
|
27
|
+
Note ids are numbers, but Word displays them sequentially by position, so inserting a note in the middle renumbers the rest automatically. Do not try to keep ids in display order — keep them unique.
|
|
28
|
+
|
|
29
|
+
## Verify
|
|
30
|
+
|
|
31
|
+
Render the page with the note and read the note area: the marker should sit immediately after the word it annotates, with no space before it, and the note text should match. Also check the last page — an endnote section that lands after the signature block is a layout defect.
|
|
32
|
+
|
|
33
|
+
## Pitfalls
|
|
34
|
+
|
|
35
|
+
- A footnote reference inside a table cell is allowed; inside a header it is not.
|
|
36
|
+
- Copying a paragraph with `python-docx` copies the reference but not the note body — recreate the note in the target document.
|
|
37
|
+
- Deleting note text without deleting the reference leaves a broken document that opens with a repair prompt.
|
|
38
|
+
- Field instructions are not allowed in the note separator; keep the built-in separator notes untouched.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Forms and content controls
|
|
2
|
+
|
|
3
|
+
**Goal.** Fill or create a structured field — a name, a date, a choice — as a real content control, so the value is labelled in the XML instead of being plain text between two labels.
|
|
4
|
+
|
|
5
|
+
A content control is `w:sdt` with `w:sdtPr` (tag, alias, id, type) and `w:sdtContent`. The tag is the machine name; the alias is what Word shows in the properties panel.
|
|
6
|
+
|
|
7
|
+
## List what exists
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/content_controls.py" list "<file.docx>"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Reports each control's part, tag, alias, id, type and current text, across the body, headers and footers. `--no_headers_footers` limits it to the body.
|
|
14
|
+
|
|
15
|
+
## Create
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
"<python>" "<skill>/scripts/content_controls.py" create "<file.docx>" \
|
|
19
|
+
--placeholder "Client name" --tag client_name --alias "Client name" --out "<file-form.docx>"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The script wraps the paragraph containing the placeholder text (or appends a new one) in a `w:sdt`, so the field is a drop-in replacement for a typed placeholder line. `--json` prints the created control instead of a summary.
|
|
23
|
+
|
|
24
|
+
## Fill
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
"<python>" "<skill>/scripts/content_controls.py" set "<file-form.docx>" \
|
|
28
|
+
--set client_name="Northwind" --set period="Q2 FY26" --out "<file-filled.docx>"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`--set TAG=VALUE` matches on the tag. The command exits 1 and lists `missing_tags` when a tag is not present — that is the check for a scripted fill, so a typo does not silently write nothing.
|
|
32
|
+
|
|
33
|
+
## Design
|
|
34
|
+
|
|
35
|
+
- One control per value; do not put a whole table into one control.
|
|
36
|
+
- Tag in `snake_case`, alias in the user's words.
|
|
37
|
+
- A choice control lists its options in `w:sdtPr`; a date control has `w:date` with a format. Plain text is the default and the safest thing to generate.
|
|
38
|
+
- Leave the surrounding labels in the body text; the alias is visible only in Word's properties panel.
|
|
39
|
+
|
|
40
|
+
## Verify
|
|
41
|
+
|
|
42
|
+
`content_controls.py list` after filling: tags present with the expected text. Then render — a control's content is normal text on the page, so the visual check is that nothing shifted and the filled value fits its line.
|
|
43
|
+
|
|
44
|
+
## Pitfalls
|
|
45
|
+
|
|
46
|
+
- Deleting an `sdt` wrapper while trying to "clear" a field deletes the value and the structure; set the text instead.
|
|
47
|
+
- Two controls with the same tag make `--set` ambiguous; the script updates the first and reports the rest.
|
|
48
|
+
- A content control cannot span two paragraphs without `w:sdtContent` holding both — keep one value per paragraph when generating.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Headings and numbering
|
|
2
|
+
|
|
3
|
+
**Goal.** A single outline a reader can follow and Word can turn into a TOC: no skipped levels, no headings faked with bold text, consistent spacing.
|
|
4
|
+
|
|
5
|
+
Heading level comes from the paragraph style (`Heading 1`…`Heading 9`, `Title` uses outline level 0), not from size or weight. `heading_audit.py` reads that structure and flags the common breakages.
|
|
6
|
+
|
|
7
|
+
## Audit
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/heading_audit.py" "<file.docx>" --max_findings 20
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Findings:
|
|
14
|
+
|
|
15
|
+
- `skipped_level` — a level 3 heading directly under a level 1. Fix by inserting the missing level or by promoting the deeper heading.
|
|
16
|
+
- `empty_heading` — a heading with no text; a stale outline entry that will appear in the TOC as a blank line.
|
|
17
|
+
- `manual_formatting` — a real heading that also carries direct bold or a large font size; the style already does that, so the extra formatting fights restyling.
|
|
18
|
+
- `heading_not_styled` — body text that looks like a heading (bold, 13pt+) but is styled `Normal`. This is the expensive one: the TOC misses it and every later style change misses it.
|
|
19
|
+
|
|
20
|
+
## Fix
|
|
21
|
+
|
|
22
|
+
Prefer styles over direct formatting. `style_normalize.py --clear_paragraph_format --enforce_heading_spacing` strips direct run formatting and sets standard space-before/after on headings:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
"<python>" "<skill>/scripts/style_normalize.py" "<file.docx>" --out "<file-fixed.docx>" --clear_paragraph_format --enforce_heading_spacing
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For a heading that is only bold text, promote it to the right style rather than making it bolder:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
"<python>" - <<'PY'
|
|
32
|
+
from docx import Document
|
|
33
|
+
d = Document(r"<file.docx>")
|
|
34
|
+
for p in d.paragraphs:
|
|
35
|
+
if p.text.strip() == "<exact heading text>":
|
|
36
|
+
p.style = d.styles["Heading 1"]
|
|
37
|
+
d.save(r"<file-fixed.docx>")
|
|
38
|
+
PY
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Numbered headings
|
|
42
|
+
|
|
43
|
+
Numbering is a numbering definition attached to the heading style, not text like `1.2`. Two consequences: a typed number keeps lying after an insert, and a numbered outline needs `numPr` on the style with an `abstractNum` that uses `%1.%2` level text. When a document must have numbered sections and the original numbering is lost, restore the style's `numPr` rather than typing numbers into the headings.
|
|
44
|
+
|
|
45
|
+
Lists are the same machinery at body level: `List Number` restarts only when the numbering definition says so, which is why two separate numbered lists sometimes continue each other.
|
|
46
|
+
|
|
47
|
+
## Verify
|
|
48
|
+
|
|
49
|
+
`heading_audit.py` should exit 0, then render and read the page: heading hierarchy visible, no orphaned heading at the bottom of a page (`keepNext`), and the same spacing before every heading of a level.
|
|
50
|
+
|
|
51
|
+
## Pitfalls
|
|
52
|
+
|
|
53
|
+
- Changing a heading's style does not change its text, but it can change its size and colour — re-render.
|
|
54
|
+
- A heading inside a table cell still counts for the outline, which is rarely wanted; use a bold paragraph there.
|
|
55
|
+
- Copying a heading from another document brings a direct-formatting payload with it; run the audit afterwards.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Images and figures
|
|
2
|
+
|
|
3
|
+
**Goal.** Pictures that are described for screen readers, sized for the page, and captioned consistently.
|
|
4
|
+
|
|
5
|
+
An inline picture is a `w:drawing` containing `wp:inline` (or `wp:anchor` for a floating one), with the picture relationship in `a:blip r:embed` and the display size in `wp:extent` (EMU). Alt text lives in `wp:docPr @descr`.
|
|
6
|
+
|
|
7
|
+
## Audit
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/images_audit.py" "<file.docx>" --max_rows 20
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For every image: the part it sits in, its name, the media file behind it, its size in inches, and the alt text. Findings are `missing_alt` and `oversized` (wider than the text block). The workable check is "does the alt text describe what a reader loses if the picture is gone".
|
|
14
|
+
|
|
15
|
+
## Fix alt text
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
"<python>" "<skill>/scripts/a11y_audit.py" "<file.docx>" --fix_image_alt from_filename --out "<file-fixed.docx>"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That writes the file name when no description exists — better than nothing, not as good as real text. For real descriptions, set `descr` yourself; decorative images get an empty description and `title`, while informative images get a sentence:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
"<python>" - <<'PY'
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
sys.path.insert(0, r"<skill>/scripts")
|
|
28
|
+
import docx_kit as kit
|
|
29
|
+
d = kit.Docx(r"<file.docx>")
|
|
30
|
+
for doc_pr in d.xml("word/document.xml").iter(kit.q("docPr", kit.WP)):
|
|
31
|
+
if doc_pr.get("name") == "Picture 3":
|
|
32
|
+
doc_pr.set("descr", "Bar chart: revenue by region, EMEA ahead of APAC.")
|
|
33
|
+
out = Path(r"<file-fixed.docx>")
|
|
34
|
+
d.save(out)
|
|
35
|
+
print("saved", out)
|
|
36
|
+
PY
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Insert
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
"<python>" "<cli>" docx create --spec "<spec.json>" --out "<out.docx>"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{ "type": "image", "path": "<absolute.png>", "width": 5.5 }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Width is in inches; the height follows the aspect ratio. An image wider than the text block gets clipped or shrinks unpredictably — 5.5in is a safe full-width figure on Letter with 1in margins.
|
|
50
|
+
|
|
51
|
+
## Figures
|
|
52
|
+
|
|
53
|
+
Caption a picture the same way as a table (`tasks/captions_crossrefs.md`): `captions_and_crossrefs.py --figures` adds `Figure N` with a `SEQ` field and a `figN` bookmark. Keep the caption in the same block so the figure and its caption do not separate across a page break.
|
|
54
|
+
|
|
55
|
+
## Verify
|
|
56
|
+
|
|
57
|
+
Render and read the page: the image is not stretched, not blurry beyond the source resolution, not clipped at the margin, and its caption sits under it. Add alt text as a separate check — the render cannot show it.
|
|
58
|
+
|
|
59
|
+
## Pitfalls
|
|
60
|
+
|
|
61
|
+
- Cropping in Word is not lossy for the file but changes the displayed extent; alt text and size checks still apply.
|
|
62
|
+
- A floating image (`wp:anchor`) can overlap text at any zoom level; prefer inline for documents that will be edited.
|
|
63
|
+
- Replacing an image file in `word/media` in place keeps the relationship but not the dimensions — the extent is stored separately.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Merge documents
|
|
2
|
+
|
|
3
|
+
**Goal.** Put one document after another and keep both looking the same — styles, numbering, images, hyperlinks — instead of pasting content that loses half of it.
|
|
4
|
+
|
|
5
|
+
Concatenating packages by hand fails on the details: style ids collide, `numId` values point at the wrong list, media parts need new names and relationships, and `r:id` values are document-scoped. Word also requires one `sectPr` at the end of the body, so the appended content has to land before it.
|
|
6
|
+
|
|
7
|
+
## Append
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/merge_docx_append.py" "<base.docx>" "<addition.docx>" --out "<merged.docx>"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
What it carries across: body content (minus the addition's final `sectPr`), media parts renamed under `word/media` with fresh relationships and content types, style definitions referenced by the copied content (following `basedOn`/`next`/`link`), numbering definitions rebuilt with fresh ids and every `w:numId` rewritten, and hyperlink relationships recreated as external relationships.
|
|
14
|
+
|
|
15
|
+
Drawings are refused by default: a floating image or a text box carries anchor geometry tied to the original page. Pass `--allow_drawings` when the user accepts that the layout may shift, then check that page carefully.
|
|
16
|
+
|
|
17
|
+
## Merge into a template
|
|
18
|
+
|
|
19
|
+
The usual job is "put these sections into the house template". Apply the template first, then append, so the appended content inherits the template's style ids:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
"<python>" "<skill>/scripts/apply_template_styles.py" --template "<house.dotx>" --target "<draft.docx>" --out "<draft-styled.docx>"
|
|
23
|
+
"<python>" "<skill>/scripts/merge_docx_append.py" "<draft-styled.docx>" "<section2.docx>" --out "<merged.docx>"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then normalize direct formatting so the merged content matches:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
"<python>" "<skill>/scripts/style_normalize.py" "<merged.docx>" --out "<merged-final.docx>" --clear_paragraph_format
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Verify
|
|
33
|
+
|
|
34
|
+
- Page count should equal the sum of the inputs, unless a page break changed.
|
|
35
|
+
- Headers and footers come from the base document; say so if the addition had its own.
|
|
36
|
+
- Render and read the join: the page where the second document starts often shows a stray page break or a heading that lost its style.
|
|
37
|
+
- `style_lint.py` on the merged file catches undefined style references imported from the addition.
|
|
38
|
+
|
|
39
|
+
## Pitfalls
|
|
40
|
+
|
|
41
|
+
- Footnotes and comments in the addition are not merged; port them deliberately if they must survive.
|
|
42
|
+
- `w:id` values for bookmarks and comments can collide across documents; the merge keeps ids and names as they were.
|
|
43
|
+
- A landscape section in the addition is not carried over — section properties were dropped with the `sectPr`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Internal navigation
|
|
2
|
+
|
|
3
|
+
**Goal.** Let a reader jump around a long document — from a contents list to a section, and back — without relying on the TOC field being recalculated.
|
|
4
|
+
|
|
5
|
+
A hyperlink to a place inside the document is `w:hyperlink w:anchor="bookmark"`; the target is a `w:bookmarkStart`/`w:bookmarkEnd` pair with that name. Both live in the body, so an export to PDF keeps the links.
|
|
6
|
+
|
|
7
|
+
## Build a navigation set
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/internal_nav.py" "<file.docx>" --out "<file-nav.docx>" --levels 1 2 \
|
|
11
|
+
--toc_title "Contents"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
What it does, in order:
|
|
15
|
+
|
|
16
|
+
1. Bookmarks every heading as `nav_h1`, `nav_h2`, … using its outline level.
|
|
17
|
+
2. Inserts a `Contents` heading and a quick-link bar (one link per heading) at the top.
|
|
18
|
+
3. Indents each contents entry by 360 twips per level so the hierarchy is visible.
|
|
19
|
+
4. Appends a small `Back to contents` link after each heading.
|
|
20
|
+
5. Bookmarks the top, the contents list and the bottom (`nav_top`, `nav_toc`, `nav_bottom`).
|
|
21
|
+
|
|
22
|
+
Flags: `--no_quicklinks` skips the horizontal bar, `--no_back_to_toc` skips the per-heading back link, `--no_top_bottom` skips the two edge targets.
|
|
23
|
+
|
|
24
|
+
The script reports `missing` when a link target could not be created — read that list, because a link without a bookmark is dead in Word and looks fine in a render.
|
|
25
|
+
|
|
26
|
+
## Static versus field-driven
|
|
27
|
+
|
|
28
|
+
`internal_nav.py` writes literal entries: it costs a rebuild when headings change, and it always looks the same in every renderer. `insert_toc.py` writes a live `TOC` field: it updates itself but needs a recalculation before it shows anything useful. A document that will be edited repeatedly wants the field; a document going out as a PDF export wants the static list.
|
|
29
|
+
|
|
30
|
+
## Verify
|
|
31
|
+
|
|
32
|
+
Render, then click through the pages you can see: entry indentation, that the contents heading is styled as a heading, and that `Back to contents` did not push a paragraph onto a new page. Dead anchors do not show in a screenshot — check `bookmarkStart` names against the `w:anchor` values when the script reports a miss.
|
|
33
|
+
|
|
34
|
+
## Pitfalls
|
|
35
|
+
|
|
36
|
+
- Bookmarks must be unique per document; re-running the script on its own output creates `nav_h1` twice, so start from the original.
|
|
37
|
+
- A hyperlink anchor is case-sensitive.
|
|
38
|
+
- Link styling comes from the `Hyperlink` character style; if links look like body text, the style is missing, not the link.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Privacy: metadata and residue
|
|
2
|
+
|
|
3
|
+
**Goal.** A file that carries only the content the user meant to send: no author name, no tracked review history, no comments, no template identity, no editing session ids.
|
|
4
|
+
|
|
5
|
+
`.docx` stores more than the text: `docProps/core.xml` (author, last modifier, title, keywords, description), `docProps/app.xml` (company, manager, template), `docProps/custom.xml`, `word/comments.xml`, revision data and per-author `w:rsid` attributes.
|
|
6
|
+
|
|
7
|
+
## Scrub metadata
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/privacy_scrub.py" "<file.docx>" --out "<file-clean.docx>"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Clears creator, last-modified-by, keywords, description, subject, title, category, company, manager and application; resets the revision counter; drops `docProps/custom.xml` and its relationships and content-type override; strips `w:rsid*` attributes, `w:proofErr` and `w:lastRenderedPageBreak` across the Word parts.
|
|
14
|
+
|
|
15
|
+
`--keep_title` leaves the title in place when the title is real content the user wants.
|
|
16
|
+
|
|
17
|
+
## The rest of the residue
|
|
18
|
+
|
|
19
|
+
| Residue | Command |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Comments and their authors | `comments_strip.py` |
|
|
22
|
+
| Tracked changes and reviewer names | `accept_tracked_changes.py --mode accept` (or reject) |
|
|
23
|
+
| Watermarks naming the old client | `watermark_audit_remove.py --mode remove` |
|
|
24
|
+
| Emails and phone numbers in the text | `redact_docx.py` |
|
|
25
|
+
|
|
26
|
+
Run them in that order — scrub metadata, resolve markup, then redact the text that survived.
|
|
27
|
+
|
|
28
|
+
## Check before delivery
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
"<python>" "<skill>/scripts/docx_kit.py" parts '<file-clean.docx>'
|
|
32
|
+
"<python>" "<skill>/scripts/docx_kit.py" xml '<file-clean.docx>' docProps/core.xml
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`parts` shows whether `docProps/custom.xml` or `word/comments.xml` still exist; the core part shows what a recipient would see in File > Info. Both are cheap and both catch the mistake the scripts did not.
|
|
36
|
+
|
|
37
|
+
## Verify
|
|
38
|
+
|
|
39
|
+
Metadata is not visible in a page render. Verify with the commands above, and render to be sure the document still looks right after the scrub — clearing `w:lastRenderedPageBreak` can change where a viewer chooses to break a long line.
|
|
40
|
+
|
|
41
|
+
## Pitfalls
|
|
42
|
+
|
|
43
|
+
- Author names also appear in comment and revision entries, not only in core properties.
|
|
44
|
+
- `docProps/app.xml` names the editing application; it is regenerated on next save by whatever app opens the file.
|
|
45
|
+
- Removing revision ids does not undo content — the text is still there.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Restrict editing
|
|
2
|
+
|
|
3
|
+
**Goal.** Stop a reviewer from changing the body while still letting them comment or track changes — the "send for review" state, not encryption.
|
|
4
|
+
|
|
5
|
+
Editing restrictions live in `w:documentProtection` inside `word/settings.xml`. The modes are `readOnly`, `comments`, `trackedChanges`, `forms`, plus `enforcement` to switch the restriction on. This is a guardrail against accidental edits; it does not encrypt content and a determined reader can remove it.
|
|
6
|
+
|
|
7
|
+
## Set a mode
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/set_protection.py" "<file.docx>" --mode comments --out "<file-review.docx>"
|
|
11
|
+
"<python>" "<skill>/scripts/set_protection.py" "<file.docx>" --mode readOnly --no_enforce --out "<file-open.docx>"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Modes and aliases: `off`/`none`, `readOnly`/`read_only`, `comments`/`comment`, `trackedChanges`/`tracked_changes`, `forms`/`form`. `--no_enforce` writes the setting without switching it on, which is useful when the opening dialog should be avoided.
|
|
15
|
+
|
|
16
|
+
`--hash` also writes a password hash (SHA-512, 100000 spins; `--salt`, `--spin_count` to change it). Be honest with the user: the hash deters casual edits, it is not encryption, and losing the password means the restriction has to be stripped from the XML.
|
|
17
|
+
|
|
18
|
+
## Which mode to pick
|
|
19
|
+
|
|
20
|
+
| Intent | Mode |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Review comments only, no text edits | `comments` |
|
|
23
|
+
| All edits should be tracked | `trackedChanges` |
|
|
24
|
+
| Fill a form, nothing else | `forms` |
|
|
25
|
+
| View only | `readOnly` |
|
|
26
|
+
|
|
27
|
+
## Remove
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
"<python>" "<skill>/scripts/set_protection.py" "<file-protected.docx>" --mode off --out "<file-open.docx>"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Verify
|
|
34
|
+
|
|
35
|
+
`docx_kit.py xml '<file.docx>' word/settings.xml | grep documentProtection` shows the element and its enforcement attribute. Then open behaviour matters, not the page — render will not show a restriction. Say which mode you applied, and that removing it needs the same script or Word's Review pane.
|
|
36
|
+
|
|
37
|
+
## Pitfalls
|
|
38
|
+
|
|
39
|
+
- Protected documents that also contain tracked changes and comments often prompt on open; apply the restriction last.
|
|
40
|
+
- `forms` mode without form fields leaves a document nobody can edit usefully.
|
|
41
|
+
- Protection marked with a hash cannot be turned off in Word without the password, but `--mode off` still clears it — that is why it is a guardrail, not security.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Read and review a file
|
|
2
|
+
|
|
3
|
+
**Goal.** Understand an existing document before touching it: what it contains, how it is structured, and what is wrong with it.
|
|
4
|
+
|
|
5
|
+
## Read the content
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
"<python>" "<cli>" docx read --input "<file.docx>"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Returns the title, author, paragraphs with their styles, headings, tables, image and equation counts, and page dimensions. Use it as the first look, including for files you produced earlier in the session.
|
|
12
|
+
|
|
13
|
+
## Read the structure
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
"<python>" "<skill>/scripts/docx_kit.py" parts '<file.docx>' # every package part and its size
|
|
17
|
+
"<python>" "<skill>/scripts/docx_kit.py" text '<file.docx>' # paragraphs with style and outline level
|
|
18
|
+
"<python>" "<skill>/scripts/docx_kit.py" xml '<file.docx>' word/document.xml
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`parts` tells you which features exist at all (a `comments.xml` means the file carries comments; its absence means none, even if the user says otherwise). `text` is faster than reading raw XML when you only need the words and their heading levels.
|
|
22
|
+
|
|
23
|
+
## Audit what is there
|
|
24
|
+
|
|
25
|
+
| Question | Command |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Are headings consistent and styled? | `heading_audit.py` |
|
|
28
|
+
| Is the prose styled directly instead of by style? | `style_lint.py` |
|
|
29
|
+
| Are images described, sized sensibly? | `images_audit.py` |
|
|
30
|
+
| Is the page setup sane? | `section_audit.py` |
|
|
31
|
+
| Are there fields, TOC entries, cross-references? | `fields_report.py` |
|
|
32
|
+
| Are there footnotes or endnotes? | `footnotes_report.py` |
|
|
33
|
+
| What comments exist, and on what text? | `comments_extract.py` |
|
|
34
|
+
|
|
35
|
+
Every audit script prints JSON and exits non-zero when it found something, so the exit code alone tells you whether the document needs attention. Read the `findings` array rather than the exit code when you need the reason.
|
|
36
|
+
|
|
37
|
+
## Review for the user
|
|
38
|
+
|
|
39
|
+
When the job is "review this document" rather than "change it":
|
|
40
|
+
|
|
41
|
+
1. Read the text, not just the audits. Point out the argument, not only the formatting.
|
|
42
|
+
2. Separate defects (broken table, skipped heading level, missing alt text) from style preferences.
|
|
43
|
+
3. Cite locations by section heading and paragraph text — Word has no stable page numbers.
|
|
44
|
+
4. Offer the fix as a concrete next action, with the command that performs it.
|
|
45
|
+
|
|
46
|
+
## Pitfalls
|
|
47
|
+
|
|
48
|
+
- `docx read` shows the body only; headers, footers, footnotes and comments need `docx_kit.py xml` or the dedicated report scripts.
|
|
49
|
+
- Tracked changes are visible as plain text in a read; use `accept_tracked_changes.py --mode report` to see what is proposed.
|
|
50
|
+
- A field's displayed text is a cached result. `fields_report.py` shows the instruction, which is the truth.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Redaction and anonymisation
|
|
2
|
+
|
|
3
|
+
**Goal.** Remove personal data — addresses, phone numbers, names, account references — from a document before it leaves the organisation, in the text and in the places text hides.
|
|
4
|
+
|
|
5
|
+
Text lives in `w:t` (and `w:delText` for deletions, `w:instrText` for field instructions). A redaction that only rewrites paragraphs via `python-docx` misses headers, footers, footnotes and comments.
|
|
6
|
+
|
|
7
|
+
## Redact
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/redact_docx.py" "<file.docx>" "<file-redacted.docx>" --emails --phones
|
|
11
|
+
"<python>" "<skill>/scripts/redact_docx.py" "<file.docx>" "<file-redacted.docx>" --pattern "ACCT-\d{6}" --replacement "<account>"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- Default run covers emails and phone numbers; add `--pattern` (repeatable) for account numbers, case ids, postcodes.
|
|
15
|
+
- `--mask_char *` keeps the shape of the value visible (`***@***.com` style), `--replacement` swaps in a token, `--no_preserve_length` stops padding to the original length.
|
|
16
|
+
- `--include_comments` also rewrites `word/comments.xml`; without it, comments keep the original text.
|
|
17
|
+
- Applied to body, every header and footer, footnotes and endnotes.
|
|
18
|
+
|
|
19
|
+
Matching happens per text run, because merging runs would destroy the surrounding formatting. A value split across runs (bolded digits inside a phone number) is not found — an acceptable trade only if you check the result; when the file mixes formatting inside the sensitive value, flatten that paragraph first (`tasks/style_lint_normalize.md`).
|
|
20
|
+
|
|
21
|
+
## Redaction versus anonymisation
|
|
22
|
+
|
|
23
|
+
Redaction removes the value; anonymisation replaces it consistently. If the same account number appears four times and the argument depends on it matching, replace all occurrences with the same token rather than `[redacted]` every time — `--replacement "<account-A>"` per pattern does that.
|
|
24
|
+
|
|
25
|
+
## Check the whole file
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
"<python>" "<skill>/scripts/docx_kit.py" text '<file-redacted.docx>' --part word/footnotes.xml
|
|
29
|
+
"<python>" "<skill>/scripts/docx_kit.py" xml '<file-redacted.docx>' word/document.xml | grep -c "example.com"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A grep over dumped XML is the honest check: run it for each sensitive term, across the document, header, footer, footnote and comment parts.
|
|
33
|
+
|
|
34
|
+
## Verify
|
|
35
|
+
|
|
36
|
+
Render, then read the pages the data appeared on. A redaction that changed the line length enough to shift the layout is worth a look; a redaction that left a `<w:t>` intact shows up in the grep, not the page.
|
|
37
|
+
|
|
38
|
+
## Pitfalls
|
|
39
|
+
|
|
40
|
+
- Field results and field instructions can both contain data; the script rewrites `w:t` and `w:delText` only.
|
|
41
|
+
- Images are not scanned — a screenshot of a spreadsheet is not redacted by any text pass. Say so plainly.
|
|
42
|
+
- Charts store category labels in `word/charts/*.xml`; inspect those parts when a chart carried the sensitive data.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Sections and page layout
|
|
2
|
+
|
|
3
|
+
**Goal.** Page size, orientation, margins, columns, headers and footers that are deliberate at every point in the document, including after a mid-document change.
|
|
4
|
+
|
|
5
|
+
Page setup lives in a `w:sectPr` element. The last one sits at the end of the body; earlier ones sit inside the last paragraph of the section they end (`w:pPr/w:sectPr`). That is why adding a landscape page means adding a section break before and after, not changing one global setting.
|
|
6
|
+
|
|
7
|
+
## Audit
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/section_audit.py" "<file.docx>"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Per section: width and height in inches, orientation derived from them, every margin, column count, section start type, header and footer parts referenced, `titlePg` (different first page) and any explicit page-number start.
|
|
14
|
+
|
|
15
|
+
Findings worth fixing: `mixed_page_sizes` (sections that disagree, usually a template artefact), `narrow_text_column` (left plus right margin eats most of the width), `portrait_wider_than_letter`.
|
|
16
|
+
|
|
17
|
+
## Common changes
|
|
18
|
+
|
|
19
|
+
| Wanted | Route |
|
|
20
|
+
|---|---|
|
|
21
|
+
| New document defaults | `docx create` spec: `page_size`, `orientation`, `top_margin`/`bottom_margin`/`left_margin`/`right_margin` in inches |
|
|
22
|
+
| A landscape page inside a portrait document | two section breaks with a landscape section between them |
|
|
23
|
+
| Different first page header | set `titlePg` on the section and fill `first_page_header` |
|
|
24
|
+
| Continuous section change (e.g. narrower margins) | `w:type val="continuous"` on the new section |
|
|
25
|
+
|
|
26
|
+
Landscape and columns are section properties, so a snippet is often faster than a flag:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
"<python>" - <<'PY'
|
|
30
|
+
from docx import Document
|
|
31
|
+
from docx.enum.section import WD_ORIENT, WD_SECTION
|
|
32
|
+
d = Document(r"<file.docx>")
|
|
33
|
+
s = d.add_section(WD_SECTION.NEW_PAGE)
|
|
34
|
+
s.orientation = WD_ORIENT.LANDSCAPE
|
|
35
|
+
s.page_width, s.page_height = s.page_height, s.page_width
|
|
36
|
+
d.save(r"<file-edited.docx>")
|
|
37
|
+
PY
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Swap the width and height explicitly: setting the orientation alone does not swap them.
|
|
41
|
+
|
|
42
|
+
## Headers and footers
|
|
43
|
+
|
|
44
|
+
A header is `word/headerN.xml` referenced from `w:headerReference`. Headers inherit from the previous section unless the section sets `headerReference` itself. Page numbers go in the footer as a `PAGE` field, page counts as `NUMPAGES`; `docx_ooxml_patch.py --add-page-numbers` writes one.
|
|
45
|
+
|
|
46
|
+
## Verify
|
|
47
|
+
|
|
48
|
+
Render and flip through every page, including the ones after a section break: header content, page numbers, orientation, margins. A section break that leaves an empty page is the classic defect — check for a stray empty paragraph carrying the break.
|
|
49
|
+
|
|
50
|
+
## Pitfalls
|
|
51
|
+
|
|
52
|
+
- `w:pgSz` is width then height; a portrait page taller than wide is normal, a "landscape" 11×8.5 that still says portrait in the panel means only the size changed.
|
|
53
|
+
- `titlePg` without filling the first-page header leaves page one with a blank header, not with the normal one.
|
|
54
|
+
- Deleting a section break merges two sections and keeps the earlier section's settings; edit before deleting.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Styles: lint and normalize
|
|
2
|
+
|
|
3
|
+
**Goal.** Formatting that comes from styles, not from pressing bold on each run, so the document can be restyled, exported and understood later.
|
|
4
|
+
|
|
5
|
+
A paragraph carries a `pStyle` reference; a run carries an `rStyle` plus direct formatting (`w:b`, `w:i`, `w:sz`, `w:color`). Direct formatting wins over the style, which is why a "make all body text 11pt" request fails on the runs someone bolded and resized by hand.
|
|
6
|
+
|
|
7
|
+
## Lint
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/style_lint.py" "<file.docx>" --json "<report.json>"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Findings: undefined style references, headings carrying their own paragraph or run formatting, empty runs, manual bullets (`•` typed into text instead of a list style), double spaces, trailing spaces, body sizes that appear once or twice against the dominant size, tables without a style.
|
|
14
|
+
|
|
15
|
+
Exit code 1 means findings; the JSON has the counts and examples, which is what you want when the number is large.
|
|
16
|
+
|
|
17
|
+
## Normalize
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
"<python>" "<skill>/scripts/style_normalize.py" "<file.docx>" --out "<file-clean.docx>" \
|
|
21
|
+
--clear_paragraph_format --enforce_heading_spacing
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `--clear_paragraph_format` strips everything from each run's `rPr` except `rStyle`, `rtl` and `lang`, and drops the paragraph-mark run properties. The style then controls the look.
|
|
25
|
+
- `--enforce_heading_spacing` sets space-before/space-after per heading level and `keepNext`, so a heading cannot sit alone at the bottom of a page.
|
|
26
|
+
|
|
27
|
+
Run this before applying a template; a template cannot win against direct formatting.
|
|
28
|
+
|
|
29
|
+
## What "correct" looks like
|
|
30
|
+
|
|
31
|
+
- Body text 11–12pt from `Normal`; headings from `Heading 1`–`Heading 3` with no direct bold or size.
|
|
32
|
+
- Bold used for emphasis inside a sentence, via a character style or a single `w:b` run — not for whole paragraphs that are really headings.
|
|
33
|
+
- Lists from `List Bullet` / `List Number`, never typed bullet characters.
|
|
34
|
+
- One table style per document, usually `Table Grid`.
|
|
35
|
+
|
|
36
|
+
## Verify
|
|
37
|
+
|
|
38
|
+
Re-run `style_lint.py` (counts should drop), then render: clearing direct formatting can change the visual hierarchy when a document faked emphasis with size. Compare the rendered page with the original before you claim the clean-up is neutral.
|
|
39
|
+
|
|
40
|
+
## Pitfalls
|
|
41
|
+
|
|
42
|
+
- Clearing `rPr` removes field-result formatting too; re-run the field-related scripts afterwards if the number must be bold.
|
|
43
|
+
- A style referenced but not defined falls back to `Normal` silently — the lint is the only warning you get.
|
|
44
|
+
- `w:rsid` and proofing attributes are noise, not style; `privacy_scrub.py` removes them when the file leaves the organisation.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Tables and spreadsheets
|
|
2
|
+
|
|
3
|
+
**Goal.** A table that fits the page, repeats its header row across pages, and survives an edit without cells drifting out of alignment.
|
|
4
|
+
|
|
5
|
+
A table's geometry is three things that must agree: the grid (`w:tblGrid`, one `w:gridCol` per column), the table width (`w:tblW`), and each cell width (`w:tcW`). When they disagree Word shows a table wider than the text block or columns that jump on the first edit.
|
|
6
|
+
|
|
7
|
+
## Create
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<cli>" docx create --spec "<spec.json>" --out "<out.docx>"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"type": "table",
|
|
16
|
+
"headers": ["Region", "Revenue", "Growth"],
|
|
17
|
+
"rows": [["EMEA", "4.1M", "12%"], ["APAC", "3.2M", "9%"]],
|
|
18
|
+
"style": "Table Grid"
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Header cells are bold. For a document with the header repeating on every page, follow up with the a11y fix, which writes `w:tblHeader` on row 1:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
"<python>" "<skill>/scripts/a11y_audit.py" "<out.docx>" --fix_table_headers first_row --out "<out-fixed.docx>"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Inspect and fix geometry
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
"<python>" "<skill>/scripts/table_geometry.py" "<file.docx>"
|
|
32
|
+
"<python>" "<skill>/scripts/table_geometry.py" "<file.docx>" --fix --out "<file-fixed.docx>"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The audit reports `tblW`, layout mode, every grid column in twips and the sum, plus the narrowest and widest cell. `--fix` writes a fixed-layout table at the text width (page width minus margins), rescales the grid proportionally and rewrites each `tcW` including `gridSpan`.
|
|
36
|
+
|
|
37
|
+
Text width on Letter with 1in margins is 9360 twips — that is the number a full-width table should total.
|
|
38
|
+
|
|
39
|
+
## Convert
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
"<python>" "<skill>/scripts/docx_table_to_csv.py" "<file.docx>" --table_index 0 --out "<table.csv>"
|
|
43
|
+
"<python>" "<skill>/scripts/xlsx_to_docx_table.py" "<book.xlsx>" --sheet Revenue --out "<table.docx>" --title "Revenue by region"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The first flattens a Word table (expanding horizontal spans, repeating vertically merged values) for data work. The second imports a worksheet as a Word table with a bold header row and `w:tblHeader`, which is the route for "put this spreadsheet in the report" — not a screenshot and not an embedded object.
|
|
47
|
+
|
|
48
|
+
## Design
|
|
49
|
+
|
|
50
|
+
- Bold header row, light grey `#D9D9D9` borders, no shading inside the body.
|
|
51
|
+
- Numeric columns right-aligned; text left-aligned. Thousands separators in the cells themselves.
|
|
52
|
+
- Two or three columns of prose at most; a wide table wants landscape, and that means a section break (`tasks/sections_layout.md`), not a smaller font.
|
|
53
|
+
- Caption above the table (`tasks/captions_crossrefs.md`), and keep it with the table.
|
|
54
|
+
- Avoid vertical merges: they look fine in the file and confuse every downstream reader.
|
|
55
|
+
|
|
56
|
+
## Verify
|
|
57
|
+
|
|
58
|
+
Render and look at the page the table lands on: nothing spills past the right margin, header row intact, no split row across pages, and the last column not truncated. A table that fits in the XML but overflows the page is a real defect, and only the render shows it.
|
|
59
|
+
|
|
60
|
+
## Pitfalls
|
|
61
|
+
|
|
62
|
+
- A table inside a table cell inherits the cell width; check the outer table first.
|
|
63
|
+
- `autofit` is a request, not a guarantee — fixed layout plus explicit widths is the only thing that renders the same everywhere.
|
|
64
|
+
- Cell margins default to 108 twips left/right; that is padding, not missing width.
|