Files
wehub-resource-sync 8cb1f9f479
Publish SDK (PyPI) / publish (push) Waiting to run
Publish SDK (npm) / publish (@aionui/officecli-sdk) (push) Waiting to run
Publish SDK (npm) / publish (@officecli/officecli-sdk) (push) Waiting to run
Publish SDK (npm) / publish (@officecli/sdk) (push) Waiting to run
Publish SDK (npm) / publish (officecli-sdk) (push) Waiting to run
SDK smoke / smoke (windows-latest) (push) Waiting to run
SDK smoke / smoke (macos-latest) (push) Waiting to run
SDK smoke / smoke (ubuntu-latest) (push) Waiting to run
Skill parity / diff (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:09:29 +08:00

274 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Sections Showcase — generates sections.docx exercising the docx `section`
property surface (schemas/help/docx/section.json): the per-section page layout
that has no per-paragraph equivalent.
A section is a run of content sharing one page layout: page size & orientation,
margins, columns, header/footer refs, line numbering, footnote/endnote behaviour
and vertical alignment. A .docx always has ONE trailing "final" section (the
body-level sectPr addressed at path "/"). Every `add / --type section` inserts a
section BREAK: it closes off the content added so far into a new /section[N]
carrying its own SectionProperties, and the still-open trailing section shifts
down to hold whatever comes next.
Pattern: add paragraphs -> add section (break) -> repeat.
The section you just added owns the paragraphs ABOVE it. Sections are
addressed /section[N] (query/get/set/remove); the final trailing section is
addressed "/" (Set rejects a break `type` there — it has no break).
This twin builds three sections with different layouts:
1. two-column portrait, page-bottom footnotes (lowerRoman, restart each page)
2. single-column landscape, vertically centered, continuous line numbering
3. two-column continuous with endnotes collected at document end
Like examples/word/document-formatting.py, this drives the officecli Python SDK
(`pip install officecli-sdk`): one resident, writes shipped over the pipe.
Usage:
python3 sections.py
"""
import os
import sys
import subprocess
try:
import officecli # pip install officecli-sdk
except ImportError:
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "..", "sdk", "python"))
import officecli
FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sections.docx")
def para(text, **props):
return {"command": "add", "parent": "/body", "type": "paragraph",
"props": {"text": text, **props}}
def section(**props):
"""A section-break `add` at path '/'. Owns the paragraphs added above it."""
return {"command": "add", "parent": "/", "type": "section", "props": props}
def footnote(para_path, text):
return {"command": "add", "parent": para_path, "type": "footnote",
"props": {"text": text}}
def endnote(para_path, text):
return {"command": "add", "parent": para_path, "type": "endnote",
"props": {"text": text}}
print("\n==========================================")
print(f"Generating sections showcase: {FILE}")
print("==========================================")
with officecli.create(FILE, "--force") as doc:
# ----------------------------------------------------------------------
# SECTION 1 — two-column newsletter body with page-bottom footnotes.
# Add the flowing content first, then the section break that owns it.
# ----------------------------------------------------------------------
# Paragraph indices: p[1] heading, p[2]..p[9] body (enough copy that
# column 1 fills top-to-bottom and text wraps into column 2).
print("\n--- Section 1: two columns + footnotes ---")
doc.batch([
para("1. Two-Column Layout with Footnotes", style="Heading1"),
para("Multi-column layout flows text down the first column, then wraps "
"to the top of the next. This section uses two balanced columns "
"with a one-centimetre gutter between them, and enough running "
"text follows that the wrap from the foot of the left column to "
"the head of the right column is plainly visible on the page."),
para("Newspapers and newsletters have used the two-column measure for "
"centuries because a narrower column is easier for the eye to "
"track: the reader's gaze travels a shorter distance before "
"returning to the start of the next line, so long passages of body "
"text feel less tiring than the same words set across the full "
"page width."),
para("Footnotes in this section are numbered with lower-case Roman "
"numerals and the counter restarts on every page, mirroring a "
"printed periodical. The reference marker sits inline in the "
"running text, while the note itself is anchored at the bottom of "
"the page column, beneath a short separator rule."),
para("Word measures the column width from the page width, minus the "
"left and right margins, minus the inter-column spacing, divided "
"across the requested column count. Because equal-width columns "
"are enabled here, both columns share exactly the same measure, "
"and the gutter between them is held at the value we set."),
para("When a column is filled to the bottom margin, the text engine "
"breaks the flow and resumes typesetting at the top of the next "
"column on the same page. Only after the last column on a page is "
"full does the flow move to a new page, so a two-column section "
"packs roughly twice as many lines onto each sheet."),
para("This is why the amount of body copy matters for a demonstration: "
"with only two or three short paragraphs the first column never "
"fills, and the layout reads on the page as if it were a single "
"column. Several paragraphs of steady prose, like these, are "
"needed before the column break actually occurs."),
para("Balancing is the final refinement. On the last page of a "
"multi-column section Word tries to even out the columns so they "
"end at roughly the same height, rather than leaving one long "
"column beside a short stub — the tidy, squared-off block of text "
"readers expect from a professionally set page."),
para("With that, the first section has enough material to spill from "
"column one into column two and, depending on font and page size, "
"perhaps onto a second page — exactly the wrapping behaviour a "
"two-column layout is meant to show."),
])
# Footnotes attach to a body paragraph; they render per footnotePr.pos.
doc.batch([
footnote("/body/p[4]", "Column width = (page width - margins - column "
"spacing) / column count."),
footnote("/body/p[8]", "Balanced columns keep the two measures visually "
"equal on the final page."),
])
# Section 1 settings: 2 columns, footnotes, A4 portrait, distinct title page.
doc.batch([section(**{
"type": "nextPage",
"pageWidth": "21cm", "pageHeight": "29.7cm", "orientation": "portrait",
"marginTop": "2.54cm", "marginBottom": "2.54cm",
"marginLeft": "2.54cm", "marginRight": "2.54cm",
"marginHeader": "1.25cm", "marginFooter": "1.25cm",
"columns": "2", "columnSpace": "1cm",
"titlePage": "true",
"footnotePr.numFmt": "lowerRoman",
"footnotePr.numRestart": "eachPage",
"footnotePr.numStart": "1",
"footnotePr.pos": "pageBottom",
})])
# ----------------------------------------------------------------------
# SECTION 2 — single-column landscape, vertically centered, line numbers.
# ----------------------------------------------------------------------
print("--- Section 2: landscape + vAlign + line numbering ---")
doc.batch([
para("2. Landscape, Single Column, Vertically Centered", style="Heading1"),
para("This section switches to landscape orientation with a single "
"column and asymmetric margins. The vertical alignment is set to "
"center, so a short block of content floats in the middle of the "
"page height instead of hugging the top margin."),
para("Landscape sections are common for wide tables, timelines and "
"figures. Because page setup is a per-section property, this page "
"can be landscape while its neighbours stay portrait, all within "
"one document."),
para("Line numbering is enabled here in continuous mode, numbering every "
"fifth line, with a gutter between the number column and the body "
"text — the layout used for legal and manuscript review."),
para("A single-column section does not wrap, so it needs no extra copy "
"to demonstrate; the point here is the interaction of landscape "
"geometry, centered vertical alignment, and the margin line numbers "
"rather than any column flow."),
])
# Section 2 settings: landscape (swapped W/H), 1 column, vAlign, line numbers.
doc.batch([section(**{
"type": "nextPage",
"orientation": "landscape",
"pageWidth": "29.7cm", "pageHeight": "21cm",
"marginTop": "2cm", "marginBottom": "2cm",
"marginLeft": "3cm", "marginRight": "1.5cm",
"columns": "1",
"vAlign": "center",
"lineNumbers": "continuous",
"lineNumberCountBy": "5",
"lineNumberDistance": "288",
"pageNumFmt": "decimal",
"pageStart": "1",
})])
# ----------------------------------------------------------------------
# SECTION 3 — continuous two-column with endnotes collected at doc end.
# ----------------------------------------------------------------------
# Paragraph indices: p[15] heading, p[16]..p[22] body (enough copy for the
# two continuous columns to fill and wrap on the page).
print("--- Section 3: continuous two columns + endnotes ---")
doc.batch([
para("3. Continuous Two-Column with Endnotes", style="Heading1"),
para("A continuous section break changes the layout without starting a "
"new page. This section returns to portrait, splits into two "
"columns again, and collects its references as endnotes gathered at "
"the end of the document rather than at the foot of each page."),
para("Because the break is continuous rather than next-page, the "
"two-column layout begins immediately below the heading on whatever "
"page the previous section left off, instead of ejecting to a fresh "
"sheet. This is the classic magazine construction: a full-width "
"headline followed by columned body text on the same page."),
para("Endnotes here use upper-case Roman numerals and restart per "
"section. Unlike footnotes, endnote bodies are not printed at the "
"foot of the page; they live in a separate store and are rendered "
"together where endnotePr.pos points them — here, at the very end."),
para("The choice between footnotes and endnotes is editorial. Footnotes "
"keep the reference in the reader's eye on the same page, which "
"suits explanatory asides, whereas endnotes keep the body text "
"clean and gather citations in one place, which suits scholarly "
"bibliographies and long reference lists."),
para("As with the first section, the wrap only becomes visible once the "
"first column fills to the bottom margin. These middle paragraphs "
"exist to supply that volume of copy, so the reader can watch the "
"text leave the foot of the left column and continue at the top of "
"the right column without a page break intervening."),
para("Continuous sections are also how a document mixes column counts on "
"a single page: a full-width introduction, then a continuous break "
"into two or three columns, then another continuous break back to "
"full width for a closing note — all flowing down the same sheet."),
para("This closing paragraph rounds out the section with enough text "
"that the two continuous columns fill and balance, completing the "
"demonstration of a continuous multi-column layout with "
"document-end endnotes."),
])
# Endnotes attach to a body paragraph, same as footnotes.
doc.batch([
endnote("/body/p[18]", "Endnotes are collected per endnotePr.pos; here "
"they gather at the document end."),
endnote("/body/p[22]", "Upper-Roman numbering restarts each section "
"under endnotePr.numRestart=eachSect."),
])
# Section 3 settings: continuous break, 2 columns, endnotes at docEnd.
doc.batch([section(**{
"type": "continuous",
"orientation": "portrait",
"pageWidth": "21cm", "pageHeight": "29.7cm",
"columns": "2", "columnSpace": "0.8cm",
"endnotePr.numFmt": "upperRoman",
"endnotePr.numRestart": "eachSect",
"endnotePr.numStart": "1",
"endnotePr.pos": "docEnd",
})])
# ----------------------------------------------------------------------
# FINAL trailing section — addressed "/" (no break type; it is the last
# one). Set page setup so the tail of the document has a defined layout.
# ----------------------------------------------------------------------
print("--- Final trailing section (path '/') ---")
doc.batch([{"command": "set", "path": "/", "props": {
"orientation": "portrait",
"pageWidth": "21cm", "pageHeight": "29.7cm",
"marginTop": "2.54cm", "marginBottom": "2.54cm",
"columns": "1", "vAlign": "top",
}}])
doc.send({"command": "save"})
# ----------------------------------------------------------------------
# Get round-trip: confirm the per-section layout keys read back. We `get`
# each /section[N] in turn (the SDK `get` mirrors CLI `get /section[N]`;
# the three break sections plus the trailing final section at "/").
# ----------------------------------------------------------------------
print("\n--- Round-trip readback (get each section) ---")
keys = ["type", "orientation", "columns", "columnSpace", "vAlign",
"footnotePr.numFmt", "endnotePr.numFmt", "lineNumbers"]
for path in ["/section[1]", "/section[2]", "/section[3]", "/"]:
node = doc.send({"command": "get", "path": path})
fmt = node.get("data", {}).get("results", [{}])[0].get("format", {})
shown = " ".join(f"{k}={fmt[k]}" for k in keys if k in fmt)
print(f" {path} {shown}")
print("\n--- Validate (fresh process, from disk) ---")
r = subprocess.run(["officecli", "validate", FILE], capture_output=True, text=True)
print(" ", (r.stdout or r.stderr).strip().split("\n")[0])
print(f"\nCreated: {FILE}")