diff --git a/.docx b/.docx deleted file mode 100644 index 34c8b9016..000000000 Binary files a/.docx and /dev/null differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c42782257..64d73990d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,33 +34,23 @@ jobs: os: ubuntu-latest - rid: win-x64 name: officecli-win-x64.exe - os: ubuntu-latest + os: windows-latest - rid: win-arm64 name: officecli-win-arm64.exe - os: ubuntu-latest + os: windows-latest runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - - name: Clone wiki for embedded help - run: git clone "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.wiki.git" src/officecli/wiki || echo "Wiki clone failed, using fallback help" + defaults: + run: + shell: bash - - name: Verify wiki version matches csproj version - run: | - CSPROJ_VERSION=$(grep '' src/officecli/officecli.csproj | sed 's/.*\(.*\)<\/Version>.*/\1/') - WIKI_VERSION=$(python3 -c "import json; print(json.load(open('src/officecli/wiki/version.json'))['version'])" 2>/dev/null || echo "unknown") - echo "csproj: $CSPROJ_VERSION, wiki: $WIKI_VERSION" - if [ "$WIKI_VERSION" != "$CSPROJ_VERSION" ]; then - echo "::error::Wiki version ($WIKI_VERSION) does not match csproj version ($CSPROJ_VERSION). Update wiki before building." - exit 1 - fi + steps: + - uses: actions/checkout@v5 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '10.0.x' - dotnet-quality: 'preview' - name: Publish run: dotnet publish src/officecli/officecli.csproj -c Release -r ${{ matrix.rid }} -o publish --nologo @@ -81,16 +71,85 @@ jobs: if: >- (matrix.rid == 'osx-arm64' && runner.arch == 'ARM64') || (matrix.rid == 'osx-x64' && runner.arch == 'X64') || - (matrix.rid == 'linux-x64' && runner.os == 'Linux') + (matrix.rid == 'linux-x64' && runner.os == 'Linux') || + (matrix.rid == 'win-x64' && runner.os == 'Windows') + env: + # Disable Git Bash (MSYS) POSIX-to-Windows path conversion on + # windows-latest, which otherwise mangles `/body` into + # `C:/Program Files/Git/body` before it reaches the CLI. + MSYS_NO_PATHCONV: '1' + MSYS2_ARG_CONV_EXCL: '*' run: | chmod +x publish/${{ matrix.name }} publish/${{ matrix.name }} create test_smoke.docx publish/${{ matrix.name }} add test_smoke.docx /body --type paragraph --prop text="Hello from CI" - publish/${{ matrix.name }} get test_smoke.docx /body/p[1] + publish/${{ matrix.name }} get test_smoke.docx '/body/p[1]' + publish/${{ matrix.name }} close test_smoke.docx rm -f test_smoke.docx + - name: Smoke test - .NET 8-only runtime (linux-x64) + if: matrix.rid == 'linux-x64' && runner.os == 'Linux' + run: | + chmod +x publish/${{ matrix.name }} + mkdir -p smoke_net8 + docker run --rm \ + -v "$PWD/publish:/app:ro" \ + -v "$PWD/smoke_net8:/work" \ + -w /work \ + mcr.microsoft.com/dotnet/runtime:8.0 \ + bash -c "set -e; \ + /app/${{ matrix.name }} create issue115.docx; \ + /app/${{ matrix.name }} add issue115.docx /body --type paragraph --prop text='net8 smoke'; \ + /app/${{ matrix.name }} close issue115.docx; \ + /app/${{ matrix.name }} view issue115.docx text --json" + + - name: Smoke test - install + if: >- + (matrix.rid == 'osx-arm64' && runner.arch == 'ARM64') || + (matrix.rid == 'osx-x64' && runner.arch == 'X64') || + (matrix.rid == 'linux-x64' && runner.os == 'Linux') || + (matrix.rid == 'win-x64' && runner.os == 'Windows') + env: + MSYS_NO_PATHCONV: '1' + MSYS2_ARG_CONV_EXCL: '*' + run: | + publish/${{ matrix.name }} install + if [ "$RUNNER_OS" == "Windows" ]; then + test -f "$LOCALAPPDATA/OfficeCLI/officecli.exe" || { echo "FAIL: officecli.exe not found in %LOCALAPPDATA%\\OfficeCLI"; exit 1; } + "$LOCALAPPDATA/OfficeCLI/officecli.exe" --version + else + test -f "$HOME/.local/bin/officecli" || { echo "FAIL: officecli not found in ~/.local/bin"; exit 1; } + "$HOME/.local/bin/officecli" --version + fi + + - name: Smoke test - install.sh / install.ps1 + # Exercises the shell installers themselves (separate from the CLI's + # own `install` subcommand tested above). Downloads from + # d.officecli.ai with github fallback — validates the production + # mirror is reachable AND the in-repo script is syntactically and + # logically correct on each platform. + if: >- + (matrix.rid == 'osx-arm64' && runner.arch == 'ARM64') || + (matrix.rid == 'osx-x64' && runner.arch == 'X64') || + (matrix.rid == 'linux-x64' && runner.os == 'Linux') || + (matrix.rid == 'win-x64' && runner.os == 'Windows') + shell: bash + env: + MSYS_NO_PATHCONV: '1' + MSYS2_ARG_CONV_EXCL: '*' + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + pwsh -NoProfile -ExecutionPolicy Bypass -File ./install.ps1 + test -f "$LOCALAPPDATA/OfficeCLI/officecli.exe" || { echo "FAIL: officecli.exe not installed"; exit 1; } + "$LOCALAPPDATA/OfficeCLI/officecli.exe" --version + else + bash install.sh + test -f "$HOME/.local/bin/officecli" || { echo "FAIL: officecli not installed at ~/.local/bin"; exit 1; } + "$HOME/.local/bin/officecli" --version + fi + - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ matrix.name }} path: publish/${{ matrix.name }} @@ -103,7 +162,7 @@ jobs: contents: write steps: - name: Download all artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts @@ -119,7 +178,7 @@ jobs: cat SHA256SUMS - name: Create Draft Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: files: artifacts/**/* generate_release_notes: true diff --git a/.github/workflows/skill-parity.yml b/.github/workflows/skill-parity.yml new file mode 100644 index 000000000..bb1bb1eb2 --- /dev/null +++ b/.github/workflows/skill-parity.yml @@ -0,0 +1,41 @@ +name: Skill parity + +# Root SKILL.md is the file the public URL serves +# (https://d.officecli.ai/SKILL.md, raw GitHub URL) and is what +# `curl ... | bash` consumers fetch. skills/officecli/SKILL.md is a +# symlink to it — the spec-conforming location that `gh skill install` +# and `npx skills add` discover, and what the binary embeds. +# +# On macOS / Linux the symlink resolves transparently and this diff +# always passes. The check exists for the Windows case: git on Windows +# without core.symlinks=true checks out symlinks as plain text files +# containing the link target path (e.g. literal "../../SKILL.md"), so +# diff catches that corruption before it ships in a release build. + +on: + push: + branches: [main] + pull_request: + paths: + - 'SKILL.md' + - 'skills/officecli/SKILL.md' + - '.github/workflows/skill-parity.yml' + +permissions: + contents: read + +jobs: + diff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Diff root SKILL.md vs skills/officecli/SKILL.md + run: | + if ! diff -q SKILL.md skills/officecli/SKILL.md; then + echo "::error::SKILL.md and skills/officecli/SKILL.md are out of sync." + echo "The two files must be byte-identical. The binary embeds skills/officecli/SKILL.md;" + echo "the public URL (d.officecli.ai/SKILL.md, raw GitHub) serves the root copy." + echo "Fix: cp SKILL.md skills/officecli/SKILL.md (or the reverse) and commit." + exit 1 + fi + echo "OK: SKILL.md == skills/officecli/SKILL.md" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..2a717d3e5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,74 @@ +# AGENTS.md — OfficeCLI Fork (AionUI-optimized) + +> **For AI agents** working with this repository. This is a **fork** of [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI) optimized for [AionUI](https://github.com/iOfficeAI/AionUi) preview with enhanced Track Changes / revision mode support. + +## Quick start (if user asked you to install this project) + +```bash +# 1. Build from source +git clone https://github.com/NextDoorLaoHuang-HF/OfficeCLI.git && cd OfficeCLI +dotnet publish src/officecli/officecli.csproj -c Release -r osx-arm64 --self-contained -o out +# Other platforms: replace osx-arm64 with osx-x64, linux-x64, linux-arm64, win-x64, or win-arm64 + +# 2. Install to PATH +sudo cp out/officecli /usr/local/bin/ # macOS/Linux +# or: copy out/officecli.exe %LOCALAPPDATA%\OfficeCli\ (Windows) + +# 3. Verify +officecli --version +``` + +## Install skills to AI agents + +```bash +officecli install # install base skill to all detected agents +officecli skills install word # install docx-specific skill +``` + +## AionUI integration (Track Changes / 修订模式) + +If the user uses **AionUI** and wants revision/track-changes capabilities: + +```bash +officecli setup-aionui +``` + +This does two things: +1. Installs the `officecli-track-changes` skill into AionUI's skills directory +2. Registers the **Word 修订助手** assistant preset in AionUI's configuration + +After running, tell the user to **restart AionUI** — the "Word 修订助手" will appear in the assistant list, pre-configured with both `officecli-docx` and `officecli-track-changes` skills. + +The `setup-aionui` command is **idempotent** — safe to run multiple times. + +### Track Changes workflow after setup + +```bash +# Preferred: find + revision (one command) +officecli set "$FILE" /body --prop find="old" --prop replace="new" --prop revision.author=AI + +# Verify +officecli query "$FILE" revision + +# Accept/reject +officecli set "$FILE" /revision --prop revision.action=accept +``` + +## Key differences from upstream + +| Feature | Upstream | This fork | +|---------|----------|-----------| +| Binary distribution | Prebuilt releases | Build from source | +| AionUI assistant | Word Creator (generic) | + Word 修订助手 (revision-focused) | +| HTML preview | Standard | Enhanced: 7 CSS classes for revision marks | +| Watch API | Standard | + /api/revision/accept\|reject\|count endpoints | +| `setup-aionui` command | None | ✅ One-shot AionUI integration | +| `officecli-track-changes` skill | None | ✅ Embedded in binary | + +## Important files + +- `SKILL.md` — base skill file (embedded in binary, installed by `officecli install`) +- `skills/officecli-docx/SKILL.md` — docx-specific skill +- `skills/officecli-track-changes/SKILL.md` — track changes / revision mode skill +- `src/officecli/Core/AionuiInstaller.cs` — `setup-aionui` implementation +- `src/officecli/Core/SkillInstaller.cs` — skill installation to AI agents diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..0b52afd76 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,126 @@ +# Contributing to OfficeCLI + +> 中文版 / Chinese version: [CONTRIBUTING.zh.md](./CONTRIBUTING.zh.md) + +> You must follow the two rules below. Code style, dependencies, tests, and +> docs are handled by the maintainer in post-merge cleanup — do not worry +> about them. + +## Rule 1: One PR = one atomic change + +A PR must contain exactly one feature or one bug fix that cannot be further +decomposed. If your change can be split into multiple pieces that each have +standalone value, submit each piece as a separate PR. + +### Self-check + +Before opening the PR, ask your AI tool: + +> "Analyze this diff. Can it be decomposed into multiple PRs where each +> could be merged or reverted independently? If yes, list them." + +If the answer is "yes, N PRs", split into N PRs before submitting. + +### Examples + +**✅ Single-PR bugs** — one root cause, one fix +- `Picture added with only 'width' specified gets wrong default height` +- `Body-level find: anchor throws ArgumentException` +- `AddParagraph --index N is off-by-one when the body contains a table` + +**✅ Single-PR features** — one coherent capability +- `query ole: list embedded OLE objects with ProgID and dimensions` +- `set wrap/hposition/vposition on floating pictures` + +**❌ Must split** — multiple independent changes bundled together +- `Fix picture index bug + add OLE detection + add HTML heading numbering` + → 3 PRs, zero shared code +- `Add OLE object detection + add EMF→PNG conversion` + → 2 PRs, two independent layers +- `Add auto aspect ratio + fix index off-by-one + fix line spacing clipping` + → 3 PRs, three unrelated root causes + +**🤔 Judgment calls** — default to splitting +- `Add helper function + its first consumer` + → 1 or 2 PRs; split if the helper has standalone reuse potential +- `Add read support + add write support for the same property` + → 1 or 2 PRs; split if you want read to land before write is vetted + +## Rule 2: Every PR must include a verifiable validation method + +State in the PR description (or a linked issue) how a reviewer can confirm +your change actually works. + +### For bug-fix PRs — pick one (in order of preference) + +1. **officecli command sequence** showing broken output before and fixed + output after +2. **Shell or Python script** that reproduces the bug and runs clean after + the fix +3. **Authoritative reference** showing what the correct behavior should be + (OOXML spec, Microsoft / ECMA docs, etc.) +4. **Screenshot** — only when the bug is purely visual + +### For feature PRs — include at minimum + +- **A screenshot** of the feature in action (Word / Excel / PowerPoint + window, HTML preview, or terminal output) +- Optionally a command sequence showing how to trigger it + +### Examples + +**Bug fix — command sequence (ideal):** + +```bash +# Before my fix: +officecli blank test.docx +officecli add test.docx picture --prop "path=photo-2x1.png" --prop "width=10cm" +officecli query test.docx picture +# → height: "10.2cm" ❌ WRONG (hardcoded 4-inch default) + +# After my fix: +officecli blank test.docx +officecli add test.docx picture --prop "path=photo-2x1.png" --prop "width=10cm" +officecli query test.docx picture +# → height: "5.0cm" ✓ CORRECT (auto-computed from 2:1 pixel ratio) +``` + +**Feature — screenshot (ideal):** + +> **Heading auto-numbering from style chain** +> +> Before: ![heading-before.png] (plain "Chapter One" with no number) +> After: ![heading-after.png] ("1. Chapter One" with auto-numbering span) +> +> How to trigger: +> ```bash +> officecli blank demo.docx +> officecli add demo.docx paragraph --prop "style=Heading1" --prop "text=Chapter One" +> officecli watch demo.docx +> ``` + +## If you don't follow these rules + +The maintainer reserves two options. + +### Option A — Reject and ask for resubmission (preferred) + +The maintainer closes the PR with a link to this guide and asks you to +resubmit as properly decomposed PRs with validation methods. + +**Your credit:** the PR is entirely yours, including the **"Merged"** badge +after resubmission. + +### Option B — Cherry-pick the valuable parts (last resort) + +If part of your PR is clearly valuable and worth saving, the maintainer runs +`git cherry-pick` on those commits into `main` directly and closes the +original PR. + +**Your credit:** +- `git cherry-pick` preserves the original author, so `git log` and + `git blame` still show you as author of those lines. +- The maintainer's reconcile commit message carries a + `Co-authored-by: ` trailer, which counts toward your + GitHub contribution graph. +- **However, the original PR shows as "Closed" instead of "Merged"**. diff --git a/CONTRIBUTING.zh.md b/CONTRIBUTING.zh.md new file mode 100644 index 000000000..4560ba6f9 --- /dev/null +++ b/CONTRIBUTING.zh.md @@ -0,0 +1,118 @@ +# 为 OfficeCLI 贡献代码 + +> English / 英文主文件: [CONTRIBUTING.md](./CONTRIBUTING.md) + +> 你必须遵守下面两条规则。代码风格、依赖、测试、文档由维护者在 merge 之后通过 +> follow-up commit 处理 —— 不用操心。 + +## Rule 1: 一个 PR 只做一件不可再拆的事 + +一个 PR 必须包含且仅包含一个 feature 或一个 bug 修复,而且这个单元不能再被拆分。 +如果你的改动可以被拆成多个每个都有独立价值的部分,就拆成多个 PR 分别提交。 + +### 自检 + +提交前,先让你的 AI 做一次拆分分析: + +> "分析下面这一坨 diff,它能不能拆成多个独立的 PR,每个都可以独立 merge 或独立 +> revert?如果可以,列出来。" + +如果回答是"可以,N 个 PR",就先拆再提。 + +### Examples + +**✅ 可以作为一个 PR 的 bug** —— 单一根因,单一修复 +- `图片只指定 width 时 height fallback 错了` +- `body 级 find: 锚点抛 ArgumentException` +- `AddParagraph --index N 在 body 含 table 时偏移` + +**✅ 可以作为一个 PR 的 feature** —— 单一 coherent 能力 +- `query ole: 列出所有嵌入的 OLE 对象及其 ProgID 和尺寸` +- `set wrap/hposition/vposition on floating pictures` + +**❌ 必须拆** —— 多个独立改动被打包 +- `修图片索引 bug + 加 OLE 检测 + 加 HTML heading 编号` + → 3 个 PR,零共享代码 +- `加 OLE 对象检测 + 加 EMF→PNG 转换` + → 2 个 PR,两个独立 layer +- `加自动宽高比 + 修索引 off-by-one + 修行距裁剪` + → 3 个 PR,三个不相关的根因 + +**🤔 可拆可不拆** —— 默认选拆 +- `加一个 helper 函数 + 第一处调用者` + → 1 或 2 个 PR;helper 有独立复用价值就拆 +- `加 read 支持 + 加 write 支持(同一属性)` + → 1 或 2 个 PR;希望 read 先被 vet 就拆 + +## Rule 2: 每个 PR 必须附带可验证的验证方法 + +在 PR description 或关联 issue 里写清楚:reviewer 怎么才能验证你的改动真的有效。 + +### Bug 修复 PR —— 至少给出一种(按优先顺序) + +1. **officecli 命令序列**,展示改动前的错误输出和改动后的正确输出 +2. **shell 或 python 脚本**,能复现 bug、在修复后干净退出 +3. **权威文档引用**,说明正确行为应该是什么样(OOXML spec、Microsoft / ECMA + 文档等) +4. **截图** —— 仅当 bug 纯粹是视觉问题时 + +### Feature PR —— 至少包含 + +- **一张截图**,展示 feature 实际效果(Word / Excel / PowerPoint 窗口、HTML + 预览、或终端输出) +- 可选:一段 shell 命令序列说明如何触发这个 feature + +### Examples + +**Bug 修复 —— 命令序列格式(最理想):** + +```bash +# Before my fix: +officecli blank test.docx +officecli add test.docx picture --prop "path=photo-2x1.png" --prop "width=10cm" +officecli query test.docx picture +# → height: "10.2cm" ❌ 错(硬编码 4 英寸 fallback) + +# After my fix: +officecli blank test.docx +officecli add test.docx picture --prop "path=photo-2x1.png" --prop "width=10cm" +officecli query test.docx picture +# → height: "5.0cm" ✓ 对(根据 2:1 像素比例自动计算) +``` + +**Feature —— 截图格式(最理想):** + +> **标题自动编号(从 style chain 解析)** +> +> Before: ![heading-before.png] (纯 "Chapter One",无编号) +> After: ![heading-after.png] ("1. Chapter One",带自动编号 span) +> +> 如何触发: +> ```bash +> officecli blank demo.docx +> officecli add demo.docx paragraph --prop "style=Heading1" --prop "text=Chapter One" +> officecli watch demo.docx +> ``` + +## 如果你不遵守这两条规则 + +维护者保留以下两种处理方式。 + +### Option A —— 拒绝并要求重新提交(首选) + +维护者关闭 PR,留一条指向本 guide 的 comment,请你按规则拆分后重新提交。 + +**你的 credit:** PR 完全归你,重新提交成功后仍然拿 **"Merged"** badge。 + +### Option B —— Cherry-pick 有价值的部分(最后手段) + +如果你的 PR 里有一部分明显有价值、值得保留,维护者会用 `git cherry-pick` 直接把 +这些 commit 摘到 `main`,然后关闭原 PR。 + +**你的 credit:** +- `git cherry-pick` 保留原作者,所以 `git log` 和 `git blame` 里那些代码行仍然 + 显示你是作者。 +- 维护者创建的 reconcile commit message 会附带 + `Co-authored-by: ` trailer,GitHub 贡献图会把它算进你的 + contribution。 +- **但原 PR 会显示为 "Closed" 而不是 "Merged"**。 diff --git a/README.md b/README.md index a2f27439d..77edefb96 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,31 @@ # OfficeCLI - -> **OfficeCLI is the world's first and the best command-line designed for AI agents.** - -**Give any AI agent full control over Word, Excel, and PowerPoint -- in one line of code.** +> **This is a fork** of [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI) maintained by [@NextDoorLaoHuang-HF](https://github.com/NextDoorLaoHuang-HF), optimized for [AionUI](https://github.com/iOfficeAI/AionUi) preview. +> +> **Differences from upstream:** +> - **HTML preview: Track Changes rendering** — revision marks (ins/del/format/move) are visually rendered in `view html` and Watch mode with distinct CSS styling +> - **Watch server: Revision API endpoints** — `POST /api/revision/accept`, `POST /api/revision/reject`, `GET /api/revision/count` for in-browser accept/reject controls +> - **Revision toolbar** — floating toolbar injected into Watch HTML with revision count badge and accept/reject buttons +> +> All changes are cleanly rebased on upstream `main` (3 commits ahead). Build as usual: `dotnet publish -c Release -r osx-arm64 --self-contained -o out`. + +> **OfficeCLI is the world's first and the best Office suite designed for AI agents.** +**Give any AI agent full control over Word, Excel, and PowerPoint — in one line of code.** Open-source. Single binary. No Office installation. No dependencies. Works everywhere. +**Built-in agent-friendly rendering engine** — agents can *see* what they create, no Office required. Render `.docx` / `.xlsx` / `.pptx` to HTML or PNG, closing the *render → look → fix* loop anywhere the binary runs. + [![GitHub Release](https://img.shields.io/github/v/release/iOfficeAI/OfficeCLI)](https://github.com/iOfficeAI/OfficeCLI/releases) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) -**English** | [中文](README_zh.md) +**English** | [中文](README_zh.md) | [日本語](README_ja.md) | [한국어](README_ko.md) + +

+ 💬 Community: Discord +

- OfficeCLI creating a PowerPoint presentation on AionUi + OfficeCLI creating a PowerPoint presentation on AionUi

PPT creation process using OfficeCLI on AionUi

@@ -66,38 +79,66 @@ curl -fsSL https://officecli.ai/SKILL.md That's it. The skill file teaches the agent how to install the binary and use all commands. -> **Technical details:** OfficeCLI ships with a [SKILL.md](SKILL.md) (239 lines, ~8K tokens) that covers command syntax, architecture, and common pitfalls. After installation, your agent can immediately create, read, and modify any Office document. +## For Humans -## Quick Start +**Option A — GUI:** Install [**AionUi**](https://github.com/iOfficeAI/AionUi) — a desktop app that lets you create and edit Office documents through natural language, powered by OfficeCLI under the hood. Just describe what you want, and AionUi handles the rest. -From zero to a finished presentation in seconds: +**Option B — CLI:** Build from source (see [Installation](#installation) below), then run: ```bash -# Create a new PowerPoint -officecli create deck.pptx +officecli install +``` -# Add a slide with a title and background color -officecli add deck.pptx / --type slide --prop title="Q4 Report" --prop background=1A1A2E +This copies the binary to your PATH and installs the **officecli skill** into every AI coding agent it detects — Claude Code, Cursor, Windsurf, GitHub Copilot, and more. Your agent can immediately create, read, and edit Office documents on your behalf, no extra configuration needed. -# Add a text shape to the slide -officecli add deck.pptx /slide[1] --type shape \ - --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm \ - --prop font=Arial --prop size=24 --prop color=FFFFFF +**AionUI users** — get Track Changes / revision mode with one command: -# View the outline of the presentation -officecli view deck.pptx outline +```bash +officecli setup-aionui ``` -Output: +This installs the `officecli-track-changes` skill and registers the **Word 修订助手** assistant in AionUI. Restart AionUI and you'll see it in the assistant list — ready for contract review, redlining, and revision workflows. +## For Developers — See It Live in 30 Seconds + +```bash +# 1. Build and install (macOS / Linux) +git clone https://github.com/NextDoorLaoHuang-HF/OfficeCLI.git && cd OfficeCLI +dotnet publish src/officecli/officecli.csproj -c Release -r osx-arm64 --self-contained -o out +sudo cp out/officecli /usr/local/bin/ + +# 2. Create a blank PowerPoint +officecli create deck.pptx + +# 3. Start live preview — opens http://localhost:26315 in your browser +officecli watch deck.pptx + +# 4. Open another terminal, add a slide — watch the browser update instantly +officecli add deck.pptx / --type slide --prop title="Hello, World!" ``` -Slide 1: Q4 Report - Shape 1 [TextBox]: Revenue grew 25% -``` + +That's it. Every `add`, `set`, or `remove` command you run will refresh the preview in real time. Keep experimenting — the browser is your live feedback loop. + +## Quick Start ```bash +# Create a presentation and add content +officecli create deck.pptx +officecli add deck.pptx / --type slide --prop title="Q4 Report" --prop background=1A1A2E +officecli add deck.pptx '/slide[1]' --type shape \ + --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm \ + --prop font=Arial --prop size=24 --prop color=FFFFFF + +# View as outline +officecli view deck.pptx outline +# → Slide 1: Q4 Report +# → Shape 1 [TextBox]: Revenue grew 25% + +# View as HTML — opens a rendered preview in your browser, no server needed +officecli view deck.pptx html + # Get structured JSON for any element -officecli get deck.pptx /slide[1]/shape[1] --json +officecli get deck.pptx '/slide[1]/shape[1]' --json ``` ```json @@ -148,11 +189,11 @@ officecli add deck.pptx / --type slide --prop title="Q4 Report" | Excel (.xlsx) | ✅ | ✅ | ✅ | | PowerPoint (.pptx) | ✅ | ✅ | ✅ | -**Word** — [paragraphs](https://github.com/iOfficeAI/OfficeCLI/wiki/word-paragraph), [runs](https://github.com/iOfficeAI/OfficeCLI/wiki/word-run), [tables](https://github.com/iOfficeAI/OfficeCLI/wiki/word-table), [styles](https://github.com/iOfficeAI/OfficeCLI/wiki/word-style), [headers/footers](https://github.com/iOfficeAI/OfficeCLI/wiki/word-header-footer), [images](https://github.com/iOfficeAI/OfficeCLI/wiki/word-picture), [equations](https://github.com/iOfficeAI/OfficeCLI/wiki/word-equation), [comments](https://github.com/iOfficeAI/OfficeCLI/wiki/word-comment), [footnotes](https://github.com/iOfficeAI/OfficeCLI/wiki/word-footnote), [watermarks](https://github.com/iOfficeAI/OfficeCLI/wiki/word-watermark), [bookmarks](https://github.com/iOfficeAI/OfficeCLI/wiki/word-bookmark), [TOC](https://github.com/iOfficeAI/OfficeCLI/wiki/word-toc), [charts](https://github.com/iOfficeAI/OfficeCLI/wiki/word-chart), [hyperlinks](https://github.com/iOfficeAI/OfficeCLI/wiki/word-hyperlink), [sections](https://github.com/iOfficeAI/OfficeCLI/wiki/word-section), [form fields](https://github.com/iOfficeAI/OfficeCLI/wiki/word-formfield), [content controls (SDT)](https://github.com/iOfficeAI/OfficeCLI/wiki/word-sdt), [fields](https://github.com/iOfficeAI/OfficeCLI/wiki/word-field), [document properties](https://github.com/iOfficeAI/OfficeCLI/wiki/word-document) +**Word** — full [i18n & RTL support](https://github.com/iOfficeAI/OfficeCLI/wiki/i18n) (per-script font slots, per-script BCP-47 lang tags `lang.latin/ea/cs`, complex-script bold/italic/size, `direction=rtl` cascading through paragraph/run/section/table/style/header/footer/docDefaults, `rtlGutter` + `pgBorders` shorthand, locale-aware page numbering for Hindi/Arabic/Thai/CJK), [paragraphs](https://github.com/iOfficeAI/OfficeCLI/wiki/word-paragraph), [runs](https://github.com/iOfficeAI/OfficeCLI/wiki/word-run), [tables](https://github.com/iOfficeAI/OfficeCLI/wiki/word-table), [styles](https://github.com/iOfficeAI/OfficeCLI/wiki/word-style), [headers/footers](https://github.com/iOfficeAI/OfficeCLI/wiki/word-header-footer), [images](https://github.com/iOfficeAI/OfficeCLI/wiki/word-picture) (PNG/JPG/GIF/SVG), [equations](https://github.com/iOfficeAI/OfficeCLI/wiki/word-equation), [comments](https://github.com/iOfficeAI/OfficeCLI/wiki/word-comment), [footnotes](https://github.com/iOfficeAI/OfficeCLI/wiki/word-footnote), [watermarks](https://github.com/iOfficeAI/OfficeCLI/wiki/word-watermark), [bookmarks](https://github.com/iOfficeAI/OfficeCLI/wiki/word-bookmark), [TOC](https://github.com/iOfficeAI/OfficeCLI/wiki/word-toc), [charts](https://github.com/iOfficeAI/OfficeCLI/wiki/word-chart), [hyperlinks](https://github.com/iOfficeAI/OfficeCLI/wiki/word-hyperlink), [sections](https://github.com/iOfficeAI/OfficeCLI/wiki/word-section), [form fields](https://github.com/iOfficeAI/OfficeCLI/wiki/word-formfield), [content controls (SDT)](https://github.com/iOfficeAI/OfficeCLI/wiki/word-sdt), [fields](https://github.com/iOfficeAI/OfficeCLI/wiki/word-field) (22 zero-param types + MERGEFIELD / REF / PAGEREF / SEQ / STYLEREF / DOCPROPERTY / IF), [OLE objects](https://github.com/iOfficeAI/OfficeCLI/wiki/word-ole), [document properties](https://github.com/iOfficeAI/OfficeCLI/wiki/word-document) -**Excel** — [cells](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-cell), formulas (150+ built-in functions with auto-evaluation), [sheets](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sheet), [tables](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-table), [conditional formatting](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-conditionalformatting), [charts](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-chart), [pivot tables](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-pivottable), [named ranges](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-namedrange), [data validation](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-validation), [images](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-picture), [sparklines](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sparkline), [comments](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-comment), [autofilter](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-autofilter), [shapes](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-shape), CSV/TSV import, `$Sheet:A1` cell addressing +**Excel** — [cells](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-cell) (phonetic guide / furigana on add), formulas (150+ built-in functions with auto-evaluation, `_xlfn.` auto-prefix for dynamic-array functions), [sheets](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sheet) (visible/hidden/veryHidden, print margins, printTitleRows/Cols, RTL `sheetView`, cascade-aware sheet rename), [tables](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-table), [sort](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sort) (sheet / range, multi-key, sidecar-aware), [conditional formatting](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-conditionalformatting), [charts](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-chart) (including box-whisker, [pareto](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-chart-add) with auto-sort + cumulative-%, log axis), [pivot tables](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-pivottable) (multi-field, date grouping, showDataAs, sort, grandTotals, subtotals, compact/outline/tabular layout, repeat item labels, blank rows, calculated fields), [slicers](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-slicer), [named ranges](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-namedrange), [data validation](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-validation), [images](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-picture) (PNG/JPG/GIF/SVG with dual-representation fallback), [sparklines](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sparkline), [comments](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-comment) (RTL), [autofilter](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-autofilter), [shapes](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-shape), [OLE objects](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-ole), CSV/TSV import, `$Sheet:A1` cell addressing -**PowerPoint** — [slides](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide), [shapes](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-shape), [images](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-picture), [tables](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-table), [charts](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-chart), [animations](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide), [morph transitions](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-morph-check), [3D models (.glb)](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-3dmodel), [slide zoom](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-zoom), [equations](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-equation), [themes](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-theme), [connectors](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-connector), [video/audio](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-video), [groups](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-group), [notes](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-notes), [placeholders](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-placeholder) +**PowerPoint** — [slides](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide) (header/footer/date/slidenum toggles, hidden), [shapes](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-shape) (pattern fill, blur effect, hyperlink tooltip + slide-jump links), [images](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-picture) (PNG/JPG/GIF/SVG, fill modes: stretch/contain/cover/tile, brightness/contrast/glow/shadow), [tables](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-table), [charts](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-chart), [animations](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide), [morph transitions](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-morph-check), [3D models (.glb)](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-3dmodel), [slide zoom](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-zoom), [equations](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-equation), [themes](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-theme), [connectors](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-connector), [video/audio](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-video), [groups](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-group), [notes](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-notes) (RTL, lang), [comments](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-comment) (RTL), [OLE objects](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-ole), [placeholders](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-placeholder) (add/set by phType) ## Use Cases @@ -173,51 +214,77 @@ officecli add deck.pptx / --type slide --prop title="Q4 Report" ## Installation -Ships as a single self-contained binary. The .NET runtime is embedded -- nothing to install, no runtime to manage. +This fork does not publish its own releases. Build from source: -**One-line install:** +**Prerequisites:** +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) ```bash -# macOS / Linux -curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash - -# Windows (PowerShell) -irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex +git clone https://github.com/NextDoorLaoHuang-HF/OfficeCLI.git +cd OfficeCLI +dotnet publish src/officecli/officecli.csproj -c Release -r osx-arm64 --self-contained -o out +sudo cp out/officecli /usr/local/bin/ ``` -**Or download manually** from [GitHub Releases](https://github.com/iOfficeAI/OfficeCLI/releases): - -| Platform | Binary | -|----------|--------| -| macOS Apple Silicon | `officecli-mac-arm64` | -| macOS Intel | `officecli-mac-x64` | -| Linux x64 | `officecli-linux-x64` | -| Linux ARM64 | `officecli-linux-arm64` | -| Windows x64 | `officecli-win-x64.exe` | -| Windows ARM64 | `officecli-win-arm64.exe` | +**Other platforms:** replace `osx-arm64` with `osx-x64`, `linux-x64`, `linux-arm64`, `win-x64`, or `win-arm64`. Verify installation: `officecli --version` -**Or self-install from a downloaded binary:** +> **Tip:** For the original upstream release with prebuilt binaries, see [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI#installation). + +## Key Features + +### Built-in Engines & Generation Primitives + +OfficeCLI is self-contained. The capabilities below ship inside the binary — **no Office required**. + +#### Rendering engine + +A from-scratch agent-friendly rendering engine ships in the binary itself, covering shapes, charts (trendlines, error bars, waterfall, candlestick, sparklines), equations (OMML → MathJax-compatible), 3D `.glb` models via Three.js, morph transitions, slide zoom, and shape effects. Per-page PNG screenshots are produced by piping the rendered HTML through a headless browser. Three modes: + +- **`view html`** — standalone HTML file, assets inlined. Open in any browser. +- **`view screenshot`** — per-page PNG, ready for multimodal agents to read. +- **`watch`** — local HTTP server with auto-refreshing preview; every `add` / `set` / `remove` updates the browser instantly. Excel watch supports inline cell editing and drag-to-reposition charts. ```bash -officecli install +officecli view deck.pptx html -o /tmp/deck.html +officecli view deck.pptx screenshot -o /tmp/deck.png # add --page 1-N for more slides +officecli watch deck.pptx # http://localhost:26315 ``` -Updates are checked automatically in the background. Disable with `officecli config autoUpdate false` or skip per-invocation with `OFFICECLI_SKIP_UPDATE=1`. Configuration lives under `~/.officecli/config.json`. +> Without visualization, an agent generating slides is flying blind — it can read the DOM but can't tell if the title overflows or two shapes overlap. Because rendering is built into the binary, the *render → look → fix* loop works in CI, in Docker, on a server with no display — anywhere the binary runs. -## Key Features +#### Formula & pivot engine -### Live Preview +150+ built-in Excel functions evaluated automatically on write — write `=SUM(A1:A2)`, `get` the cell, the value is already there. No round-trip through Office to recalc. Covers dynamic-array functions (`FILTER` / `UNIQUE` / `SORT` / `SEQUENCE` with auto `_xlfn.` prefix), `VLOOKUP` / `INDEX` / `MATCH`, date & text functions, and 140+ more. -`watch` starts a local HTTP server with a live HTML preview of your PowerPoint file. Every modification auto-refreshes in the browser — ideal for iterative design with AI agents. +Plus native OOXML pivot tables from a source range with one command — multi-field rows/cols/filters, 10 aggregations, `showDataAs` modes, date grouping, calculated fields, top-N, layouts. Pivot cache + definition are written to OOXML, so Excel opens the file with the aggregation already populated: ```bash -officecli watch deck.pptx -# Opens http://localhost:18080 — refreshes on every set/add/remove +officecli add sales.xlsx '/Sheet1' --type pivottable \ + --prop source='Data!A1:E10000' --prop rows='Region,Category' \ + --prop cols=Quarter --prop values='Revenue:sum,Units:avg' \ + --prop showDataAs=percentOfTotal ``` -Renders shapes, charts, equations, 3D models (Three.js), morph transitions, zoom navigation, and all shape effects. +#### Template merge — generate once, fill many + +`merge` replaces `{{key}}` placeholders in any `.docx` / `.xlsx` / `.pptx` with JSON data — across paragraphs, table cells, shapes, headers, footers, and chart titles. Agent designs the layout once (expensive); production code fills it N times (cheap, deterministic, zero token cost). Avoids the failure mode where an agent regenerates each report from scratch and produces N inconsistent layouts. + +```bash +officecli merge invoice-template.docx out-001.docx '{"client":"Acme","total":"$5,200"}' +officecli merge q4-template.pptx q4-acme.pptx data.json +``` + +#### Round-trip dump — learn from existing docs + +`dump` serializes any `.docx` — whole document **or any subtree** (a single paragraph, table, the styles part, numbering, theme, or settings) — into a replayable batch JSON; `batch` replays it. Given a sample the user wants to imitate, an agent reads the structured spec instead of raw OOXML XML, mutates, and replays. Bridges "I have an existing template" and "generate me 100 variations." + +```bash +officecli dump existing.docx -o blueprint.json # whole document +officecli dump existing.docx /body/tbl[1] -o table.json # any subtree +officecli batch new.docx --input blueprint.json +``` ### Resident Mode & Batch @@ -230,10 +297,16 @@ officecli set report.docx /body/p[1]/r[1] --prop bold=true officecli set report.docx /body/p[2]/r[1] --prop color=FF0000 officecli close report.docx -# Batch mode — atomic multi-command execution +# Batch mode — atomic multi-command execution (stops on first error by default) echo '[{"command":"set","path":"/slide[1]/shape[1]","props":{"text":"Hello"}}, {"command":"set","path":"/slide[1]/shape[2]","props":{"fill":"FF0000"}}]' \ - | officecli batch deck.pptx --stop-on-error + | officecli batch deck.pptx --json + +# Inline batch with --commands (no stdin needed) +officecli batch deck.pptx --commands '[{"op":"set","path":"/slide[1]/shape[1]","props":{"text":"Hi"}}]' + +# Use --force to continue past errors +officecli batch deck.pptx --input updates.json --force --json ``` ### Three-Layer Architecture @@ -242,8 +315,8 @@ Start simple, go deep only when needed. | Layer | Purpose | Commands | |-------|---------|----------| -| **L1: Read** | Semantic views of content | `view` (text, annotated, outline, stats, issues, html) | -| **L2: DOM** | Structured element operations | `get`, `query`, `set`, `add`, `remove`, `move` | +| **L1: Read** | Semantic views of content | `view` (text, annotated, outline, stats, issues, html, svg, screenshot) | +| **L2: DOM** | Structured element operations | `get`, `query`, `set`, `add`, `remove`, `move`, `swap` | | **L3: Raw XML** | Direct XPath access — universal fallback | `raw`, `raw-set`, `add-part`, `validate` | ```bash @@ -257,7 +330,7 @@ officecli add budget.xlsx / --type sheet --prop name="Q2 Report" officecli move report.docx /body/p[5] --to /body --index 1 # L3 — raw XML when L2 isn't enough -officecli raw deck.pptx /slide[1] +officecli raw deck.pptx '/slide[1]' officecli raw-set report.docx document \ --xpath "//w:p[1]" --action append \ --xml 'Injected text' @@ -303,37 +376,22 @@ curl -fsSL https://officecli.ai/SKILL.md curl -fsSL https://officecli.ai/SKILL.md -o ~/.claude/skills/officecli.md ``` -**Other agents:** Include the contents of `SKILL.md` (239 lines, ~8K tokens) in your agent's system prompt or tool description. +**Other agents:** Include the contents of `SKILL.md` in your agent's system prompt or tool description. -**Call from any language:** - -```python -# Python -import subprocess, json -def cli(*args): return subprocess.check_output(["officecli", *args], text=True) -cli("create", "deck.pptx") -cli("set", "deck.pptx", "/slide[1]/shape[1]", "--prop", "text=Hello") -``` - -```js -// JavaScript -const { execFileSync } = require('child_process') -const cli = (...args) => execFileSync('officecli', args, { encoding: 'utf8' }) -cli('set', 'deck.pptx', '/slide[1]/shape[1]', '--prop', 'text=Hello') -``` - -Every command supports `--json` for structured output. Path-based addressing means agents don't need to understand XML namespaces. +### Why your agent will thrive on OfficeCLI -### Why agents love OfficeCLI - -- **Deterministic JSON output** -- Every command supports `--json`, returning structured data with consistent schemas. No regex parsing needed. -- **Path-based addressing** -- Every element has a stable path (`/slide[1]/shape[2]`). Agents navigate documents without understanding XML namespaces. Note: these paths use OfficeCLI's own syntax (1-based indexing, element local names), not XPath. -- **Progressive complexity** -- Start with L1 (read), escalate to L2 (modify), fall back to L3 (raw XML) only when needed. Minimizes token usage. -- **Self-healing workflow** -- `validate`, `view issues`, and the help system let agents detect problems and self-correct without human intervention. -- **Built-in help** -- When unsure about property names or value formats, run `officecli set ` instead of guessing. -- **Auto-install** -- No manual skill-file setup. OfficeCLI detects your AI tools and configures itself automatically. +- **Deterministic JSON output** — every command supports `--json` with consistent schemas. No regex parsing, no scraping stdout. +- **Path-based addressing** — every element has a stable path (`/slide[1]/shape[2]`). Agents navigate documents without understanding XML namespaces. (OfficeCLI syntax: 1-based indexing, element local names — not XPath.) +- **Progressive complexity (L1 → L2 → L3)** — agents start with read-only views, escalate to DOM ops, fall back to raw XML only when needed. Minimizes token usage. +- **Self-healing workflow** — `validate`, `view issues`, and the structured error codes (`not_found`, `invalid_value`, `unsupported_property`) return suggestions and valid ranges. Agents self-correct without human intervention. +- **Built-in agent-friendly rendering engine** — `view html` / `view screenshot` / `watch` emit HTML and PNG natively. No Office required. Agents can *see* their output and fix layout issues, even inside CI / Docker / headless environments. +- **Built-in formula & pivot engine** — 150+ Excel functions auto-evaluated on write; native OOXML pivot tables from a source range with one command. Agents read computed values and shipped aggregations immediately, without round-tripping through Office. +- **Template merge** — agent designs the layout once, downstream code fills `{{key}}` placeholders N times. Avoids burning tokens regenerating every report from scratch. +- **Round-trip dump** — `dump` turns any `.docx` into replayable batch JSON. Agents learn from human-authored samples by reading a structured spec, not raw OOXML XML. +- **Built-in help** — when unsure about property names or value formats, the agent runs `officecli set ` instead of guessing. +- **Auto-install** — OfficeCLI detects your AI tooling (Claude Code, Cursor, VS Code, …) and configures itself. No manual skill-file setup. ### Built-in Help @@ -412,33 +470,35 @@ See `officecli --help` for full details on exit codes and error formats. | Call from any language | ✓ (CLI) | ✗ (COM/Add-in) | ✗ (UNO API) | Python only | | Path-based element access | ✓ | ✗ | ✗ | ✗ | | Raw XML fallback | ✓ | ✗ | ✗ | Partial | -| Live preview | ✓ | ✓ | ✗ | ✗ | +| Built-in agent-friendly rendering engine | ✓ | ✗ | ✗ | ✗ | +| Headless HTML/PNG output | ✓ | ✗ | Partial | ✗ | +| Template merge (`{{key}}`) across formats | ✓ | ✗ | ✗ | ✗ | +| Round-trip dump → batch JSON | ✓ | ✗ | ✗ | ✗ | +| Live preview (auto-refresh on edit) | ✓ | ✗ | ✗ | ✗ | | Headless / CI | ✓ | ✗ | Partial | ✓ | | Cross-platform | ✓ | Windows/Mac | ✓ | ✓ | | Word + Excel + PowerPoint | ✓ | ✓ | ✓ | Separate libs | -## Updates & Configuration - -```bash -officecli config autoUpdate false # Disable auto-update checks -OFFICECLI_SKIP_UPDATE=1 officecli ... # Skip check for one invocation (CI) -``` - ## Command Reference | Command | Description | |---------|-------------| | [`create`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-create) | Create a blank .docx, .xlsx, or .pptx (type from extension) | -| [`view`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-view) | View content (modes: `outline`, `text`, `annotated`, `stats`, `issues`, `html`) | +| [`view`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-view) | View content (modes: `outline`, `text`, `annotated`, `stats` (`--page-count`), `issues`, `html`, `svg`, `screenshot`, `pdf` (via exporter plugin), `forms` (via format-handler plugin)). docx supports `--render auto\|native\|html`. | +| [`load_skill`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-skills) | Print embedded SKILL.md content for a specialized skill (no install) | | [`get`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-get) | Get element and children (`--depth N`, `--json`) | | [`query`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-query) | CSS-like query (`[attr=value]`, `:contains()`, `:has()`, etc.) | | [`set`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-set) | Modify element properties | | [`add`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-add) | Add element (or clone with `--from `) | | [`remove`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-remove) | Remove an element | -| [`move`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-move) | Move element (`--to --index N`) | +| [`move`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-move) | Move element (`--to `, `--index N`, `--after `, `--before `) | | [`swap`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-swap) | Swap two elements | | [`validate`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-validate) | Validate against OpenXML schema | -| [`batch`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-batch) | Multiple operations in one open/save cycle (JSON on stdin or `--input`) | +| `view issues` | Enumerate document issues (text overflow, missing alt text, formula errors, ...) | +| [`batch`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-batch) | Multiple operations in one open/save cycle (stdin, `--input`, or `--commands`; stops on first error, `--force` to continue) | +| [`dump`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-dump) | Serialize a `.docx` or `.pptx` into a replayable batch JSON (round-trip via `batch`); accepts a subtree path | +| [`refresh`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-refresh) | Recalculate TOC page numbers / `PAGE` / cross-references (`.docx`; Word backend on Windows, headless-HTML fallback) | +| [`plugins`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-plugins) | List / inspect / lint installed plugins (extend to `.doc`, `.hwpx`, `.pdf` export via dump-reader / exporter / format-handler kinds) | | [`merge`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-merge) | Template merge — replace `{{key}}` placeholders with JSON data | | [`watch`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-watch) | Live HTML preview in browser with auto-refresh | | [`mcp`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-mcp) | Start MCP server for AI tool integration | @@ -461,10 +521,10 @@ officecli create report.pptx # 2. Add content officecli add report.pptx / --type slide --prop title="Q4 Results" -officecli add report.pptx /slide[1] --type shape \ +officecli add report.pptx '/slide[1]' --type shape \ --prop text="Revenue: $4.2M" --prop x=2cm --prop y=5cm --prop size=28 officecli add report.pptx / --type slide --prop title="Details" -officecli add report.pptx /slide[2] --type shape \ +officecli add report.pptx '/slide[2]' --type shape \ --prop text="Growth driven by new markets" --prop x=2cm --prop y=5cm # 3. Verify @@ -474,22 +534,7 @@ officecli validate report.pptx # 4. Fix any issues found officecli view report.pptx issues --json # Address issues based on output, e.g.: -officecli set report.pptx /slide[1]/shape[1] --prop font=Arial -``` - -### Template Merge - -Replace `{{key}}` placeholders in any document with JSON data -- works across paragraphs, table cells, shapes, headers, footers, and chart titles. - -```bash -# Merge from inline JSON -officecli merge template.docx output.docx '{"name":"Alice","dept":"Sales","date":"2026-03-30"}' - -# Merge from a JSON file -officecli merge template.pptx report.pptx data.json - -# Excel template -officecli merge budget-template.xlsx q4-budget.xlsx '{"quarter":"Q4","year":"2026"}' +officecli set report.pptx '/slide[1]/shape[1]' --prop font=Arial ``` ### Units & Colors @@ -526,6 +571,20 @@ officecli merge invoice-template.docx invoice-001.docx '{"client":"Acme","total" officecli validate report.docx && officecli view report.docx issues --json ``` +**From Python** — wrap once, get parsed JSON back from every call: + +```python +import json, subprocess + +def cli(*args): + return json.loads(subprocess.check_output(["officecli", *args, "--json"], text=True)) + +cli("create", "deck.pptx") +cli("add", "deck.pptx", "/", "--type", "slide", "--prop", "title=Q4 Report") +slide = cli("get", "deck.pptx", "/slide[1]") +print(slide["attributes"]["text"]) +``` + ## Documentation The [Wiki](https://github.com/iOfficeAI/OfficeCLI/wiki) has detailed guides for every command, element type, and property: @@ -551,6 +610,8 @@ Bug reports and contributions are welcome on [GitHub Issues](https://github.com/ --- +If you find OfficeCLI useful, please [give it a star on GitHub](https://github.com/iOfficeAI/OfficeCLI) — it helps others discover the project. + [OfficeCLI.AI](https://OfficeCLI.AI) | [GitHub](https://github.com/iOfficeAI/OfficeCLI) @@ -583,7 +644,7 @@ keywords: office, cli, ai-agent, automation, docx, xlsx, pptx, openxml, document ai-agent-compatible: true mcp-server: true skill-file: SKILL.md -skill-file-lines: 239 +skill-file-lines: 403 alternatives: python-docx, openpyxl, python-pptx, libreoffice --headless install-command-unix: curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash install-command-windows: irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex diff --git a/README_ja.md b/README_ja.md new file mode 100644 index 000000000..9be6144fd --- /dev/null +++ b/README_ja.md @@ -0,0 +1,646 @@ +# OfficeCLI + +> **OfficeCLI は世界初にして最高の、AI エージェント向けに設計された Office スイートです。** + +**あらゆる AI エージェントに Word、Excel、PowerPoint の完全な制御権を — たった一行のコードで。** + +オープンソース。単一バイナリ。Office のインストール不要。依存関係ゼロ。全プラットフォーム対応。 + +**エージェントフレンドリーなレンダリングエンジンを内蔵** — エージェントは自分が作ったものを "見る" ことができ、Office 不要。`.docx` / `.xlsx` / `.pptx` を HTML または PNG にレンダリングし、"レンダリング → 見る → 修正" のループはバイナリが動くあらゆる場所で完結します。 + +[![GitHub Release](https://img.shields.io/github/v/release/iOfficeAI/OfficeCLI)](https://github.com/iOfficeAI/OfficeCLI/releases) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) + +[English](README.md) | [中文](README_zh.md) | **日本語** | [한국어](README_ko.md) + +

+ 💬 コミュニティ: Discord +

+ +

+ AionUi で OfficeCLI を使った PPT 作成プロセス +

+ +

AionUi で OfficeCLI を使った PPT 作成プロセス

+ +

PowerPoint プレゼンテーション

+ + + + + + + + + + + + +
OfficeCLI デザインプレゼン (PowerPoint)OfficeCLI ビジネスプレゼン (PowerPoint)OfficeCLI テクノロジープレゼン (PowerPoint)
OfficeCLI 宇宙プレゼン (PowerPoint)OfficeCLI ゲームプレゼン (PowerPoint)OfficeCLI クリエイティブプレゼン (PowerPoint)
+ +

+

Word 文書

+ + + + + + + +
OfficeCLI 学術論文 (Word)OfficeCLI プロジェクト提案書 (Word)OfficeCLI 年次報告書 (Word)
+ +

+

Excel スプレッドシート

+ + + + + + + +
OfficeCLI 予算管理 (Excel)OfficeCLI 成績管理 (Excel)OfficeCLI 売上ダッシュボード (Excel)
+ +

上記の文書はすべて AI エージェントが OfficeCLI を使って全自動で作成 — テンプレートなし、手動編集なし。

+ +## AI エージェント向け — 一行で開始 + +これを AI エージェントのチャットに貼り付けるだけ — スキルファイルを自動で読み込み、インストールを完了します: + +``` +curl -fsSL https://officecli.ai/SKILL.md +``` + +これだけです。スキルファイルがエージェントにバイナリのインストール方法と全コマンドの使い方を教えます。 + +## 一般ユーザー向け + +**オプション A — GUI:** [**AionUi**](https://github.com/iOfficeAI/AionUi) をインストール — 自然言語で Office 文書を作成・編集できるデスクトップアプリ。内部で OfficeCLI が動いています。やりたいことを説明するだけで、AionUi がすべて処理します。 + +**オプション B — CLI:** [GitHub Releases](https://github.com/iOfficeAI/OfficeCLI/releases) からお使いのプラットフォーム用バイナリをダウンロードして、以下を実行: + +```bash +officecli install +``` + +バイナリを PATH にコピーし、検出されたすべての AI コーディングエージェント(Claude Code、Cursor、Windsurf、GitHub Copilot など)に **officecli スキル**を自動インストールします。エージェントはすぐに Office 文書の作成・読み取り・編集が可能になります。追加設定は不要です。 + +## 開発者向け — 30秒でライブ体験 + +```bash +# 1. インストール(macOS / Linux) +curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash +# Windows (PowerShell): irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex + +# 2. 空の PowerPoint を作成 +officecli create deck.pptx + +# 3. ライブプレビューを開始 — ブラウザで http://localhost:26315 が開きます +officecli watch deck.pptx + +# 4. 別のターミナルを開いてスライドを追加 — ブラウザが即座に更新されます +officecli add deck.pptx / --type slide --prop title="Hello, World!" +``` + +これだけです。`add`、`set`、`remove` コマンドを実行するたびに、プレビューがリアルタイムで更新されます。どんどん試してみてください — ブラウザがあなたのライブフィードバックループです。 + +## クイックスタート + +```bash +# プレゼンテーションを作成してコンテンツを追加 +officecli create deck.pptx +officecli add deck.pptx / --type slide --prop title="Q4 Report" --prop background=1A1A2E +officecli add deck.pptx '/slide[1]' --type shape \ + --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm \ + --prop font=Arial --prop size=24 --prop color=FFFFFF + +# アウトラインを表示 +officecli view deck.pptx outline +# → Slide 1: Q4 Report +# → Shape 1 [TextBox]: Revenue grew 25% + +# HTML で表示 — サーバー不要、ブラウザでレンダリングされたプレビューを開きます +officecli view deck.pptx html + +# 任意の要素の構造化 JSON を取得 +officecli get deck.pptx '/slide[1]/shape[1]' --json +``` + +```json +{ + "tag": "shape", + "path": "/slide[1]/shape[1]", + "attributes": { + "name": "TextBox 1", + "text": "Revenue grew 25%", + "x": "720000", + "y": "1800000" + } +} +``` + +## なぜ OfficeCLI? + +以前は 50行の Python と 3つのライブラリが必要でした: + +```python +from pptx import Presentation +from pptx.util import Inches, Pt +prs = Presentation() +slide = prs.slides.add_slide(prs.slide_layouts[0]) +title = slide.shapes.title +title.text = "Q4 Report" +# ... さらに 45行 ... +prs.save('deck.pptx') +``` + +今はコマンド一つで: + +```bash +officecli add deck.pptx / --type slide --prop title="Q4 Report" +``` + +**OfficeCLI でできること:** + +- **作成** ドキュメント -- 空白またはコンテンツ付き +- **読み取り** テキスト、構造、スタイル、数式 -- プレーンテキストまたは構造化 JSON +- **分析** フォーマットの問題、スタイルの不整合、構造的な欠陥 +- **修正** 任意の要素 -- テキスト、フォント、色、レイアウト、数式、チャート、画像 +- **再構成** コンテンツ -- 要素の追加、削除、移動、文書間コピー + +| フォーマット | 読み取り | 修正 | 作成 | +|-------------|---------|------|------| +| Word (.docx) | ✅ | ✅ | ✅ | +| Excel (.xlsx) | ✅ | ✅ | ✅ | +| PowerPoint (.pptx) | ✅ | ✅ | ✅ | + +**Word** — 完全な [i18n & RTL サポート](https://github.com/iOfficeAI/OfficeCLI/wiki/i18n)(スクリプト別フォントスロット、スクリプト別 BCP-47 言語タグ `lang.latin/ea/cs`、複雑スクリプトの太字/斜体/サイズ、段落/ラン/セクション/表/スタイル/ヘッダー/フッター/docDefaults をカスケードする `direction=rtl`、`rtlGutter` + `pgBorders` ショートハンド、ヒンディー語/アラビア語/タイ語/CJK のロケール対応ページ番号)、[段落](https://github.com/iOfficeAI/OfficeCLI/wiki/word-paragraph)、[ラン](https://github.com/iOfficeAI/OfficeCLI/wiki/word-run)、[表](https://github.com/iOfficeAI/OfficeCLI/wiki/word-table)、[スタイル](https://github.com/iOfficeAI/OfficeCLI/wiki/word-style)、[ヘッダー/フッター](https://github.com/iOfficeAI/OfficeCLI/wiki/word-header-footer)、[画像](https://github.com/iOfficeAI/OfficeCLI/wiki/word-picture)(PNG/JPG/GIF/SVG)、[数式](https://github.com/iOfficeAI/OfficeCLI/wiki/word-equation)、[コメント](https://github.com/iOfficeAI/OfficeCLI/wiki/word-comment)、[脚注](https://github.com/iOfficeAI/OfficeCLI/wiki/word-footnote)、[透かし](https://github.com/iOfficeAI/OfficeCLI/wiki/word-watermark)、[ブックマーク](https://github.com/iOfficeAI/OfficeCLI/wiki/word-bookmark)、[目次](https://github.com/iOfficeAI/OfficeCLI/wiki/word-toc)、[チャート](https://github.com/iOfficeAI/OfficeCLI/wiki/word-chart)、[ハイパーリンク](https://github.com/iOfficeAI/OfficeCLI/wiki/word-hyperlink)、[セクション](https://github.com/iOfficeAI/OfficeCLI/wiki/word-section)、[フォームフィールド](https://github.com/iOfficeAI/OfficeCLI/wiki/word-formfield)、[コンテンツコントロール (SDT)](https://github.com/iOfficeAI/OfficeCLI/wiki/word-sdt)、[フィールド](https://github.com/iOfficeAI/OfficeCLI/wiki/word-field)(22 種類のゼロ引数 + MERGEFIELD / REF / PAGEREF / SEQ / STYLEREF / DOCPROPERTY / IF)、[OLE オブジェクト](https://github.com/iOfficeAI/OfficeCLI/wiki/word-ole)、[文書プロパティ](https://github.com/iOfficeAI/OfficeCLI/wiki/word-document) + +**Excel** — [セル](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-cell)(追加時にふりがな対応)、数式(150以上の組み込み関数を自動計算、動的配列関数に `_xlfn.` 自動プレフィックス)、[シート](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sheet)(visible/hidden/veryHidden、印刷余白、printTitleRows/Cols、RTL `sheetView`、カスケード対応のシート名変更)、[テーブル](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-table)、[ソート](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sort)(シート/範囲、マルチキー、サイドカー対応)、[条件付き書式](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-conditionalformatting)、[チャート](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-chart)(箱ひげ図、[パレート図](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-chart-add) 自動ソート + 累積%、対数軸を含む)、[ピボットテーブル](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-pivottable)(マルチフィールド、日付グループ化、showDataAs、ソート、総計、小計、コンパクト/アウトライン/表形式レイアウト、項目ラベル繰り返し、空白行、計算フィールド)、[スライサー](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-slicer)、[名前付き範囲](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-namedrange)、[データ入力規則](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-validation)、[画像](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-picture)(PNG/JPG/GIF/SVG、デュアル表現フォールバック)、[スパークライン](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sparkline)、[コメント](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-comment)(RTL)、[オートフィルター](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-autofilter)、[図形](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-shape)、[OLE オブジェクト](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-ole)、CSV/TSV インポート、`$Sheet:A1` セルアドレッシング + +**PowerPoint** — [スライド](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide)(ヘッダー/フッター/日付/スライド番号トグル、非表示)、[図形](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-shape)(パターン塗りつぶし、ぼかし効果、ハイパーリンクツールチップ + スライドジャンプリンク)、[画像](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-picture)(PNG/JPG/GIF/SVG、塗りモード: stretch/contain/cover/tile、明るさ/コントラスト/光彩/影)、[表](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-table)、[チャート](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-chart)、[アニメーション](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide)、[モーフトランジション](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-morph-check)、[3D モデル (.glb)](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-3dmodel)、[スライドズーム](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-zoom)、[数式](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-equation)、[テーマ](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-theme)、[コネクタ](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-connector)、[ビデオ/オーディオ](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-video)、[グループ](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-group)、[ノート](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-notes)(RTL、lang)、[コメント](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-comment)(RTL)、[OLE オブジェクト](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-ole)、[プレースホルダー](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-placeholder)(phType で追加/設定) + +## 使用シーン + +**開発者向け:** +- データベースや API からのレポート自動生成 +- 文書の一括処理(一括検索/置換、スタイル更新) +- CI/CD 環境でのドキュメントパイプライン構築(テスト結果からドキュメント生成) +- Docker/コンテナ環境でのヘッドレス Office 自動化 + +**AI エージェント向け:** +- ユーザーのプロンプトからプレゼンテーションを生成(上記の例を参照) +- ドキュメントから構造化データを JSON に抽出 +- 納品前のドキュメント品質検証 + +**チーム向け:** +- ドキュメントテンプレートを複製してデータを入力 +- CI/CD パイプラインでの自動ドキュメント検証 + +## インストール + +単一の自己完結型バイナリとして配布。.NET ランタイムは内蔵 -- インストール不要、ランタイム管理不要。 + +**ワンライナーインストール:** + +```bash +# macOS / Linux +curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash + +# Windows (PowerShell) +irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex +``` + +**または手動ダウンロード** [GitHub Releases](https://github.com/iOfficeAI/OfficeCLI/releases): + +| プラットフォーム | バイナリ | +|----------------|---------| +| macOS Apple Silicon | `officecli-mac-arm64` | +| macOS Intel | `officecli-mac-x64` | +| Linux x64 | `officecli-linux-x64` | +| Linux ARM64 | `officecli-linux-arm64` | +| Windows x64 | `officecli-win-x64.exe` | +| Windows ARM64 | `officecli-win-arm64.exe` | + +インストール確認:`officecli --version` + +**またはダウンロード済みバイナリからセルフインストール(`officecli` を直接実行してもインストールがトリガーされます):** + +```bash +officecli install # 明示的インストール +officecli # 直接実行でもインストールがトリガー +``` + +更新はバックグラウンドで自動チェックされます。`officecli config autoUpdate false` で無効化、または `OFFICECLI_SKIP_UPDATE=1` で単回スキップ可能。設定は `~/.officecli/config.json` にあります。 + +## 主な機能 + +### 内蔵エンジンと生成プリミティブ + +OfficeCLI は自己完結型です。以下の機能はすべてバイナリ内蔵 — **Office 不要**。 + +#### レンダリングエンジン + +ゼロから実装したエージェントフレンドリーなレンダリングエンジンがバイナリ内に同梱され、シェイプ、チャート (トレンドライン、エラーバー、ウォーターフォール、ローソク足、スパークライン)、数式 (OMML → MathJax 互換)、Three.js による 3D `.glb` モデル、モーフトランジション、スライドズーム、シェイプエフェクトをカバー。ページごとの PNG スクリーンショットは、レンダリングされた HTML をヘッドレスブラウザに渡して生成されます。3 つのモード: + +- **`view html`** — スタンドアロン HTML ファイル、アセットインライン。任意のブラウザで開けます。 +- **`view screenshot`** — ページごとの PNG、マルチモーダルエージェント向け。 +- **`watch`** — ローカル HTTP サーバー + 自動更新プレビュー。`add` / `set` / `remove` でブラウザが即座に更新。Excel watch はインラインセル編集とチャートのドラッグ再配置をサポート。 + +```bash +officecli view deck.pptx html -o /tmp/deck.html +officecli view deck.pptx screenshot -o /tmp/deck.png # 複数ページは --page 1-N +officecli watch deck.pptx # http://localhost:26315 +``` + +> 可視化なしでは、スライドを生成するエージェントは盲目的に飛んでいるようなもの — DOM は読めても、タイトルがオーバーフローしているか、2 つのシェイプが重なっているかは判断できません。レンダリングがバイナリに内蔵されているため、"レンダリング → 見る → 修正" のループは CI、Docker、ディスプレイのないサーバー — バイナリが動くあらゆる場所で動作します。 + +#### 数式 & ピボットエンジン + +150+ の Excel 関数が書き込み時に自動評価 — `=SUM(A1:A2)` を書いて、セルを `get` する、値はすでにそこに。Office で再計算するラウンドトリップは不要。動的配列関数 (`FILTER` / `UNIQUE` / `SORT` / `SEQUENCE`、`_xlfn.` 自動プレフィックス)、`VLOOKUP` / `INDEX` / `MATCH`、日付・テキスト関数など 140+ の関数をカバー。 + +加えて、ソース範囲から 1 コマンドでネイティブな OOXML ピボットテーブル — マルチフィールドの行/列/フィルター、10 種類の集計、`showDataAs` モード、日付グループ化、計算フィールド、Top-N、レイアウト。ピボットキャッシュ + 定義は OOXML に書き込まれ、Excel で開くと集計済みの状態で表示されます: + +```bash +officecli add sales.xlsx '/Sheet1' --type pivottable \ + --prop source='Data!A1:E10000' --prop rows='Region,Category' \ + --prop cols=Quarter --prop values='Revenue:sum,Units:avg' \ + --prop showDataAs=percentOfTotal +``` + +#### テンプレートマージ — 一度設計、N 回入力 + +`merge` は任意の `.docx` / `.xlsx` / `.pptx` の `{{key}}` プレースホルダーを JSON データで置換 — 段落、表セル、シェイプ、ヘッダー/フッター、チャートタイトル全体で動作。エージェントが一度レイアウトを設計 (高コスト)、本番コードが N 回入力 (低コスト、決定論的、トークンコストゼロ)。エージェントが各レポートを毎回ゼロから再生成し、N 個の一貫性のないレイアウトを生み出す失敗モードを回避します。 + +```bash +officecli merge invoice-template.docx out-001.docx '{"client":"Acme","total":"$5,200"}' +officecli merge q4-template.pptx q4-acme.pptx data.json +``` + +#### Dump によるラウンドトリップ — 既存ドキュメントから学ぶ + +`dump` は任意の `.docx` — ドキュメント全体**または任意のサブツリー**(単一の段落、表、styles、numbering、theme、settings)— を再生可能なバッチ JSON にシリアライズし、`batch` で再生。ユーザーが模倣したいサンプルから、エージェントは生の OOXML XML ではなく構造化された仕様を読み、変更して再生します。"既存テンプレートがある" と "100 個のバリエーションを生成して" を繋ぎます。 + +```bash +officecli dump existing.docx -o blueprint.json # ドキュメント全体 +officecli dump existing.docx /body/tbl[1] -o table.json # 任意のサブツリー +officecli batch new.docx --input blueprint.json +``` + +### レジデントモードとバッチ + +複数ステップのワークフローでは、レジデントモードがドキュメントをメモリに保持。バッチモードは一度の open/save サイクルで複数操作を実行します。 + +```bash +# レジデントモード — 名前付きパイプ経由で遅延ほぼゼロ +officecli open report.docx +officecli set report.docx /body/p[1]/r[1] --prop bold=true +officecli set report.docx /body/p[2]/r[1] --prop color=FF0000 +officecli close report.docx + +# バッチモード — アトミックなマルチコマンド実行(デフォルトで最初のエラーで停止) +echo '[{"command":"set","path":"/slide[1]/shape[1]","props":{"text":"Hello"}}, + {"command":"set","path":"/slide[1]/shape[2]","props":{"fill":"FF0000"}}]' \ + | officecli batch deck.pptx --json + +# インラインバッチ — stdin 不要 +officecli batch deck.pptx --commands '[{"op":"set","path":"/slide[1]/shape[1]","props":{"text":"Hi"}}]' + +# --force でエラーをスキップして続行 +officecli batch deck.pptx --input updates.json --force --json +``` + +### 三層アーキテクチャ + +シンプルに始めて、必要な時だけ深く。 + +| レイヤー | 用途 | コマンド | +|---------|------|---------| +| **L1:読み取り** | コンテンツのセマンティックビュー | `view`(text、annotated、outline、stats、issues、html、svg、screenshot) | +| **L2:DOM** | 構造化された要素操作 | `get`、`query`、`set`、`add`、`remove`、`move`、`swap` | +| **L3:生 XML** | XPath による直接アクセス — 万能フォールバック | `raw`、`raw-set`、`add-part`、`validate` | + +```bash +# L1 — 高レベルビュー +officecli view report.docx annotated +officecli view budget.xlsx text --cols A,B,C --max-lines 50 + +# L2 — 要素レベルの操作 +officecli query report.docx "run:contains(TODO)" +officecli add budget.xlsx / --type sheet --prop name="Q2 Report" +officecli move report.docx /body/p[5] --to /body --index 1 + +# L3 — L2 では足りない時に生 XML +officecli raw deck.pptx '/slide[1]' +officecli raw-set report.docx document \ + --xpath "//w:p[1]" --action append \ + --xml 'Injected text' +``` + +## AI 統合 + +### MCP サーバー + +組み込み [MCP](https://modelcontextprotocol.io) サーバー — コマンド一つで登録: + +```bash +officecli mcp claude # Claude Code +officecli mcp cursor # Cursor +officecli mcp vscode # VS Code / Copilot +officecli mcp lmstudio # LM Studio +officecli mcp list # 登録状態を確認 +``` + +JSON-RPC で全ドキュメント操作を公開 — シェルアクセス不要。 + +### 直接 CLI 統合 + +2ステップで OfficeCLI を任意の AI エージェントに統合: + +1. **バイナリをインストール** -- コマンド一つ([インストール](#インストール)参照) +2. **完了。** OfficeCLI は AI ツール(Claude Code、GitHub Copilot、Codex)を自動検出し、既知の設定ディレクトリを確認してスキルファイルをインストールします。エージェントはすぐに Office 文書の作成・読み取り・変更が可能です。 + +
+手動設定(オプション) + +自動インストールがお使いの環境に対応していない場合、手動でスキルファイルをインストールできます: + +**SKILL.md を直接エージェントに読み込ませる:** + +```bash +curl -fsSL https://officecli.ai/SKILL.md +``` + +**Claude Code のローカルスキルとしてインストール:** + +```bash +curl -fsSL https://officecli.ai/SKILL.md -o ~/.claude/skills/officecli.md +``` + +**その他のエージェント:** `SKILL.md` の内容をエージェントのシステムプロンプトまたはツール説明に含めてください。 + +
+ +### エージェントが OfficeCLI で活躍する理由 + +- **決定論的 JSON 出力** — すべてのコマンドが `--json` をサポートし、スキーマは一貫。正規表現パース不要、stdout スクレイピング不要。 +- **パスベースのアドレッシング** — すべての要素に安定したパス (`/slide[1]/shape[2]`)。エージェントは XML 名前空間を理解せずにドキュメントをナビゲート可能。(OfficeCLI 独自の構文: 1-based インデックス、要素ローカル名 — XPath ではない。) +- **段階的複雑度 (L1 → L2 → L3)** — エージェントは読み取り専用ビューから始め、DOM 操作にエスカレート、必要な時のみ raw XML にフォールバック。トークン消費を最小化。 +- **自己修復ワークフロー** — `validate`、`view issues`、構造化エラーコード (`not_found`、`invalid_value`、`unsupported_property`) は suggestion と有効範囲を返します。エージェントは人間の介入なしに自己修正します。 +- **内蔵エージェントフレンドリーレンダリングエンジン** — `view html` / `view screenshot` / `watch` がネイティブに HTML と PNG を出力。Office 不要。エージェントは CI / Docker / ヘッドレス環境でも自分の出力を "見て" レイアウトの問題を修正できます。 +- **内蔵数式 & ピボットエンジン** — 150+ の Excel 関数が書き込み時に自動評価; ソース範囲から 1 コマンドでネイティブ OOXML ピボットテーブル。エージェントは Office で再計算せずに、計算値と集計結果を即座に読み取れます。 +- **テンプレートマージ** — エージェントがレイアウトを一度設計し、下流コードが `{{key}}` プレースホルダーを N 回入力。各レポートを再生成してトークンを焼くことを避けます。 +- **ラウンドトリップ Dump** — `dump` が任意の `.docx` を再生可能なバッチ JSON に変換。エージェントは生の OOXML XML ではなく構造化された仕様を読んで、人間が作成したサンプルから学習。 +- **内蔵ヘルプ** — プロパティ名や値形式に迷ったら、エージェントは推測せず `officecli set ` を実行。 +- **自動インストール** — OfficeCLI は使っているツール (Claude Code、Cursor、VS Code…) を検出して自己構成します。手動の skill ファイルセットアップ不要。 + +### 組み込みヘルプ + +プロパティ名がわからない時は、階層型ヘルプで確認: + +```bash +officecli pptx set # 全設定可能な要素とプロパティ +officecli pptx set shape # 特定の要素タイプの詳細 +officecli pptx set shape.fill # 単一プロパティのフォーマットと例 +officecli docx query # セレクタリファレンス:属性、:contains、:has() など +``` + +`pptx` を `docx` や `xlsx` に置き換え可能。動詞は `view`、`get`、`query`、`set`、`add`、`raw`。 + +`officecli --help` で全体概要を確認。 + +### JSON 出力スキーマ + +全コマンドが `--json` に対応。一般的なレスポンス形式: + +**単一要素**(`get --json`): + +```json +{"tag": "shape", "path": "/slide[1]/shape[1]", "attributes": {"name": "TextBox 1", "text": "Hello"}} +``` + +**要素リスト**(`query --json`): + +```json +[ + {"tag": "paragraph", "path": "/body/p[1]", "attributes": {"style": "Heading1", "text": "Title"}}, + {"tag": "paragraph", "path": "/body/p[5]", "attributes": {"style": "Heading1", "text": "Summary"}} +] +``` + +**エラー** は構造化エラーオブジェクトを返却。エラーコード、修正提案、利用可能な値を含みます: + +```json +{ + "success": false, + "error": { + "error": "Slide 50 not found (total: 8)", + "code": "not_found", + "suggestion": "Valid Slide index range: 1-8" + } +} +``` + +エラーコード:`not_found`、`invalid_value`、`unsupported_property`、`invalid_path`、`unsupported_type`、`missing_property`、`file_not_found`、`file_locked`、`invalid_selector`。プロパティ名は自動修正対応 -- プロパティ名のスペルミスは最も近い候補を提案します。 + +**エラー回復** -- エージェントは利用可能な要素を確認して自己修正: + +```bash +# エージェントが無効なパスを試行 +officecli get report.docx /body/p[99] --json +# 返却: {"success": false, "error": {"error": "...", "code": "not_found", "suggestion": "..."}} + +# エージェントが利用可能な要素を確認して自己修正 +officecli get report.docx /body --depth 1 --json +# 利用可能な子要素のリストを返却、エージェントが正しいパスを選択 +``` + +**変更確認**(`set`、`add`、`remove`、`move`、`create` で `--json` 使用時): + +```json +{"success": true, "path": "/slide[1]/shape[1]"} +``` + +`officecli --help` で終了コードとエラー形式の完全な説明を確認。 + +## 比較 + +| | OfficeCLI | Microsoft Office | LibreOffice | python-docx / openpyxl | +|---|---|---|---|---| +| オープンソース&無料 | ✓ (Apache 2.0) | ✗(有料ライセンス) | ✓ | ✓ | +| AI ネイティブ CLI + JSON | ✓ | ✗ | ✗ | ✗ | +| ゼロインストール(単一バイナリ) | ✓ | ✗ | ✗ | ✗(Python + pip 必要) | +| 任意の言語から呼び出し | ✓ (CLI) | ✗ (COM/Add-in) | ✗ (UNO API) | Python のみ | +| パスベースの要素アクセス | ✓ | ✗ | ✗ | ✗ | +| 生 XML フォールバック | ✓ | ✗ | ✗ | 部分対応 | +| 内蔵エージェントフレンドリーレンダリングエンジン | ✓ | ✗ | ✗ | ✗ | +| ヘッドレス HTML/PNG 出力 | ✓ | ✗ | 部分対応 | ✗ | +| クロスフォーマットテンプレートマージ (`{{key}}`) | ✓ | ✗ | ✗ | ✗ | +| Dump → batch JSON ラウンドトリップ | ✓ | ✗ | ✗ | ✗ | +| ライブプレビュー (編集後自動更新) | ✓ | ✗ | ✗ | ✗ | +| ヘッドレス / CI | ✓ | ✗ | 部分対応 | ✓ | +| クロスプラットフォーム | ✓ | Windows/Mac | ✓ | ✓ | +| Word + Excel + PowerPoint | ✓ | ✓ | ✓ | 複数ライブラリが必要 | + +## コマンドリファレンス + +| コマンド | 説明 | +|---------|------| +| [`create`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-create) | 空白の .docx、.xlsx、.pptx を作成(拡張子からタイプを判定) | +| [`view`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-view) | コンテンツを表示(モード:`outline`、`text`、`annotated`、`stats`、`issues`、`html`) | +| [`get`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-get) | 要素と子要素を取得(`--depth N`、`--json`) | +| [`query`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-query) | CSS スタイルのクエリ(`[attr=value]`、`:contains()`、`:has()` など) | +| [`set`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-set) | 要素のプロパティを変更 | +| [`add`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-add) | 要素を追加(または `--from ` でクローン) | +| [`remove`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-remove) | 要素を削除 | +| [`move`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-move) | 要素を移動(`--to `、`--index N`、`--after `、`--before `) | +| [`swap`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-swap) | 2つの要素を交換 | +| [`validate`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-validate) | OpenXML スキーマ検証 | +| [`batch`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-batch) | 一度の open/save サイクルで複数操作を実行(stdin、`--input`、または `--commands`;デフォルトで最初のエラーで停止、`--force` で続行) | +| [`merge`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-merge) | テンプレートマージ — `{{key}}` プレースホルダーを JSON データで置換 | +| [`watch`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-watch) | ブラウザでライブ HTML プレビュー、自動更新 | +| [`mcp`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-mcp) | AI ツール統合用の MCP サーバーを起動 | +| [`raw`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-raw) | ドキュメントパートの生 XML を表示 | +| [`raw-set`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-raw) | XPath で生 XML を変更 | +| `add-part` | 新しいドキュメントパート(ヘッダー、チャートなど)を追加 | +| [`open`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-open) | レジデントモードを開始(ドキュメントをメモリに保持) | +| `close` | 保存してレジデントモードを終了 | +| [`install`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-install) | バイナリ + スキル + MCP をインストール(`all`、`claude`、`cursor` など) | +| `config` | 設定の取得または変更 | +| ` ` | [組み込みヘルプ](https://github.com/iOfficeAI/OfficeCLI/wiki/command-reference)(例:`officecli pptx set shape`) | + +## エンドツーエンドワークフロー例 + +典型的なエージェント自己修復ワークフロー:プレゼンテーションの作成、コンテンツの入力、検証、問題の修正 -- すべて人間の介入なし。 + +```bash +# 1. 作成 +officecli create report.pptx + +# 2. コンテンツを追加 +officecli add report.pptx / --type slide --prop title="Q4 Results" +officecli add report.pptx '/slide[1]' --type shape \ + --prop text="Revenue: $4.2M" --prop x=2cm --prop y=5cm --prop size=28 +officecli add report.pptx / --type slide --prop title="Details" +officecli add report.pptx '/slide[2]' --type shape \ + --prop text="Growth driven by new markets" --prop x=2cm --prop y=5cm + +# 3. 検証 +officecli view report.pptx outline +officecli validate report.pptx + +# 4. 問題の修正 +officecli view report.pptx issues --json +# 出力に基づいて問題を修正: +officecli set report.pptx '/slide[1]/shape[1]' --prop font=Arial +``` + +### 単位と色 + +すべての寸法・色プロパティは柔軟な入力形式に対応: + +| タイプ | 対応形式 | 例 | +|-------|---------|-----| +| **寸法** | cm、in、pt、px または生 EMU | `2cm`、`1in`、`72pt`、`96px`、`914400` | +| **色** | 16進数、色名、RGB、テーマ色 | `#FF0000`、`FF0000`、`red`、`rgb(255,0,0)`、`accent1` | +| **フォントサイズ** | 数値のみまたは pt 接尾辞付き | `14`、`14pt`、`10.5pt` | +| **間隔** | pt、cm、in または倍率 | `12pt`、`0.5cm`、`1.5x`、`150%` | + +## よく使うパターン + +```bash +# Word 文書の全 Heading1 テキストを置換 +officecli query report.docx "paragraph[style=Heading1]" --json | ... +officecli set report.docx /body/p[1]/r[1] --prop text="New Title" + +# 全スライドのコンテンツを JSON でエクスポート +officecli get deck.pptx / --depth 2 --json + +# Excel セルを一括更新 +officecli batch budget.xlsx --input updates.json --json + +# CSV データを Excel シートにインポート +officecli add budget.xlsx / --type sheet --prop name="Q1 Data" --prop csv=sales.csv + +# テンプレートマージでレポートを一括生成 +officecli merge invoice-template.docx invoice-001.docx '{"client":"Acme","total":"$5,200"}' + +# 納品前にドキュメント品質をチェック +officecli validate report.docx && officecli view report.docx issues --json +``` + +**Python から呼び出し** — 一度ラップすれば、すべての呼び出しでパース済み JSON が返ります: + +```python +import json, subprocess + +def cli(*args): + return json.loads(subprocess.check_output(["officecli", *args, "--json"], text=True)) + +cli("create", "deck.pptx") +cli("add", "deck.pptx", "/", "--type", "slide", "--prop", "title=Q4 レポート") +slide = cli("get", "deck.pptx", "/slide[1]") +print(slide["attributes"]["text"]) +``` + +## ドキュメント + +[Wiki](https://github.com/iOfficeAI/OfficeCLI/wiki) に全コマンド、要素タイプ、プロパティの詳細ガイドがあります: + +- **フォーマット別:**[Word](https://github.com/iOfficeAI/OfficeCLI/wiki/word-reference) | [Excel](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-reference) | [PowerPoint](https://github.com/iOfficeAI/OfficeCLI/wiki/powerpoint-reference) +- **ワークフロー:**[エンドツーエンド例](https://github.com/iOfficeAI/OfficeCLI/wiki/workflows) -- Word レポート、Excel ダッシュボード、PPT プレゼン、一括変更、レジデントモード +- **トラブルシューティング:**[よくあるエラーと解決策](https://github.com/iOfficeAI/OfficeCLI/wiki/troubleshooting) +- **AI エージェントガイド:**[Wiki ナビゲーション決定木](https://github.com/iOfficeAI/OfficeCLI/wiki/agent-guide) + +## ソースからビルド + +コンパイルには [.NET 10 SDK](https://dotnet.microsoft.com/download) が必要です。出力は自己完結型のネイティブバイナリ -- .NET は内蔵されているため、実行時にはインストール不要です。 + +```bash +./build.sh +``` + +## ライセンス + +[Apache License 2.0](LICENSE) + +バグ報告やコントリビューションは [GitHub Issues](https://github.com/iOfficeAI/OfficeCLI/issues) まで。 + +--- + +OfficeCLI が役に立ったら、ぜひ [GitHub でスターを付けてください](https://github.com/iOfficeAI/OfficeCLI) — より多くの人にプロジェクトを届ける力になります。 + +[OfficeCLI.AI](https://OfficeCLI.AI) | [GitHub](https://github.com/iOfficeAI/OfficeCLI) + + + + diff --git a/README_ko.md b/README_ko.md new file mode 100644 index 000000000..44889e184 --- /dev/null +++ b/README_ko.md @@ -0,0 +1,646 @@ +# OfficeCLI + +> **OfficeCLI는 세계 최초이자 최고의, AI 에이전트를 위해 설계된 Office 스위트입니다.** + +**모든 AI 에이전트에게 Word, Excel, PowerPoint의 완전한 제어권을 — 단 한 줄의 코드로.** + +오픈소스. 단일 바이너리. Office 설치 불필요. 의존성 제로. 모든 플랫폼 지원. + +**에이전트 친화적 렌더링 엔진 내장** — 에이전트가 자신이 만든 것을 "볼" 수 있고, Office 불필요. `.docx` / `.xlsx` / `.pptx`를 HTML 또는 PNG로 렌더링하며, *렌더링 → 보기 → 수정* 루프는 바이너리가 실행되는 어디서나 닫힙니다. + +[![GitHub Release](https://img.shields.io/github/v/release/iOfficeAI/OfficeCLI)](https://github.com/iOfficeAI/OfficeCLI/releases) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) + +[English](README.md) | [中文](README_zh.md) | [日本語](README_ja.md) | **한국어** + +

+ 💬 커뮤니티: Discord +

+ +

+ AionUi에서 OfficeCLI로 PPT 제작 과정 +

+ +

AionUi에서 OfficeCLI로 PPT 제작 과정

+ +

PowerPoint 프레젠테이션

+ + + + + + + + + + + + +
OfficeCLI 디자인 프레젠테이션 (PowerPoint)OfficeCLI 비즈니스 프레젠테이션 (PowerPoint)OfficeCLI 테크 프레젠테이션 (PowerPoint)
OfficeCLI 우주 프레젠테이션 (PowerPoint)OfficeCLI 게임 프레젠테이션 (PowerPoint)OfficeCLI 크리에이티브 프레젠테이션 (PowerPoint)
+ +

+

Word 문서

+ + + + + + + +
OfficeCLI 학술 논문 (Word)OfficeCLI 프로젝트 제안서 (Word)OfficeCLI 연간 보고서 (Word)
+ +

+

Excel 스프레드시트

+ + + + + + + +
OfficeCLI 예산 관리 (Excel)OfficeCLI 성적 관리 (Excel)OfficeCLI 매출 대시보드 (Excel)
+ +

위의 모든 문서는 AI 에이전트가 OfficeCLI를 사용하여 완전 자동으로 생성 — 템플릿 없음, 수동 편집 없음.

+ +## AI 에이전트용 — 한 줄로 시작 + +이 한 줄을 AI 에이전트 채팅에 붙여넣기만 하면 — 스킬 파일을 자동으로 읽고 설치를 완료합니다: + +``` +curl -fsSL https://officecli.ai/SKILL.md +``` + +이게 전부입니다. 스킬 파일이 에이전트에게 바이너리 설치 방법과 모든 명령어 사용법을 알려줍니다. + +## 일반 사용자용 + +**옵션 A — GUI:** [**AionUi**](https://github.com/iOfficeAI/AionUi)를 설치하세요 — 자연어로 Office 문서를 만들고 편집할 수 있는 데스크톱 앱입니다. 내부적으로 OfficeCLI가 구동됩니다. 원하는 것을 설명하기만 하면 AionUi가 모든 것을 처리합니다. + +**옵션 B — CLI:** [GitHub Releases](https://github.com/iOfficeAI/OfficeCLI/releases)에서 플랫폼에 맞는 바이너리를 다운로드한 후 실행: + +```bash +officecli install +``` + +바이너리를 PATH에 복사하고, 감지된 모든 AI 코딩 에이전트(Claude Code, Cursor, Windsurf, GitHub Copilot 등)에 **officecli 스킬**을 자동 설치합니다. 에이전트는 즉시 Office 문서를 생성, 읽기, 편집할 수 있으며 추가 설정이 필요 없습니다. + +## 개발자용 — 30초 만에 라이브로 확인 + +```bash +# 1. 설치 (macOS / Linux) +curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash +# Windows (PowerShell): irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex + +# 2. 빈 PowerPoint 생성 +officecli create deck.pptx + +# 3. 라이브 미리보기 시작 — 브라우저에서 http://localhost:26315 이 열립니다 +officecli watch deck.pptx + +# 4. 다른 터미널을 열고 슬라이드 추가 — 브라우저가 즉시 업데이트됩니다 +officecli add deck.pptx / --type slide --prop title="Hello, World!" +``` + +이게 전부입니다. `add`, `set`, `remove` 명령을 실행할 때마다 미리보기가 실시간으로 갱신됩니다. 계속 실험해 보세요 — 브라우저가 바로 여러분의 라이브 피드백 루프입니다. + +## 빠른 시작 + +```bash +# 프레젠테이션을 생성하고 콘텐츠 추가 +officecli create deck.pptx +officecli add deck.pptx / --type slide --prop title="Q4 Report" --prop background=1A1A2E +officecli add deck.pptx '/slide[1]' --type shape \ + --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm \ + --prop font=Arial --prop size=24 --prop color=FFFFFF + +# 개요 보기 +officecli view deck.pptx outline +# → Slide 1: Q4 Report +# → Shape 1 [TextBox]: Revenue grew 25% + +# HTML로 보기 — 서버 없이 브라우저에서 렌더링된 미리보기를 엽니다 +officecli view deck.pptx html + +# 모든 요소의 구조화된 JSON 가져오기 +officecli get deck.pptx '/slide[1]/shape[1]' --json +``` + +```json +{ + "tag": "shape", + "path": "/slide[1]/shape[1]", + "attributes": { + "name": "TextBox 1", + "text": "Revenue grew 25%", + "x": "720000", + "y": "1800000" + } +} +``` + +## 왜 OfficeCLI인가? + +이전에는 50줄의 Python과 3개의 라이브러리가 필요했습니다: + +```python +from pptx import Presentation +from pptx.util import Inches, Pt +prs = Presentation() +slide = prs.slides.add_slide(prs.slide_layouts[0]) +title = slide.shapes.title +title.text = "Q4 Report" +# ... 45줄 더 ... +prs.save('deck.pptx') +``` + +이제 명령어 하나면 됩니다: + +```bash +officecli add deck.pptx / --type slide --prop title="Q4 Report" +``` + +**OfficeCLI로 할 수 있는 것:** + +- **생성** 문서 -- 빈 문서 또는 콘텐츠 포함 +- **읽기** 텍스트, 구조, 스타일, 수식 -- 일반 텍스트 또는 구조화된 JSON +- **분석** 서식 문제, 스타일 불일치, 구조적 결함 +- **수정** 모든 요소 -- 텍스트, 글꼴, 색상, 레이아웃, 수식, 차트, 이미지 +- **재구성** 콘텐츠 -- 요소 추가, 삭제, 이동, 문서 간 복사 + +| 형식 | 읽기 | 수정 | 생성 | +|------|------|------|------| +| Word (.docx) | ✅ | ✅ | ✅ | +| Excel (.xlsx) | ✅ | ✅ | ✅ | +| PowerPoint (.pptx) | ✅ | ✅ | ✅ | + +**Word** — 완전한 [i18n 및 RTL 지원](https://github.com/iOfficeAI/OfficeCLI/wiki/i18n) (스크립트별 글꼴 슬롯, 스크립트별 BCP-47 언어 태그 `lang.latin/ea/cs`, 복합 스크립트 굵게/기울임/크기, 단락/런/섹션/표/스타일/머리글/바닥글/docDefaults에 캐스케이드되는 `direction=rtl`, `rtlGutter` + `pgBorders` 단축형, 힌디/아랍어/태국어/CJK 로캘 인식 페이지 번호), [단락](https://github.com/iOfficeAI/OfficeCLI/wiki/word-paragraph), [런](https://github.com/iOfficeAI/OfficeCLI/wiki/word-run), [표](https://github.com/iOfficeAI/OfficeCLI/wiki/word-table), [스타일](https://github.com/iOfficeAI/OfficeCLI/wiki/word-style), [머리글/바닥글](https://github.com/iOfficeAI/OfficeCLI/wiki/word-header-footer), [이미지](https://github.com/iOfficeAI/OfficeCLI/wiki/word-picture) (PNG/JPG/GIF/SVG), [수식](https://github.com/iOfficeAI/OfficeCLI/wiki/word-equation), [메모](https://github.com/iOfficeAI/OfficeCLI/wiki/word-comment), [각주](https://github.com/iOfficeAI/OfficeCLI/wiki/word-footnote), [워터마크](https://github.com/iOfficeAI/OfficeCLI/wiki/word-watermark), [북마크](https://github.com/iOfficeAI/OfficeCLI/wiki/word-bookmark), [목차](https://github.com/iOfficeAI/OfficeCLI/wiki/word-toc), [차트](https://github.com/iOfficeAI/OfficeCLI/wiki/word-chart), [하이퍼링크](https://github.com/iOfficeAI/OfficeCLI/wiki/word-hyperlink), [섹션](https://github.com/iOfficeAI/OfficeCLI/wiki/word-section), [양식 필드](https://github.com/iOfficeAI/OfficeCLI/wiki/word-formfield), [콘텐츠 컨트롤 (SDT)](https://github.com/iOfficeAI/OfficeCLI/wiki/word-sdt), [필드](https://github.com/iOfficeAI/OfficeCLI/wiki/word-field) (22개 무인수 + MERGEFIELD / REF / PAGEREF / SEQ / STYLEREF / DOCPROPERTY / IF), [OLE 객체](https://github.com/iOfficeAI/OfficeCLI/wiki/word-ole), [문서 속성](https://github.com/iOfficeAI/OfficeCLI/wiki/word-document) + +**Excel** — [셀](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-cell) (추가 시 음성 가이드/후리가나), 수식(150개 이상의 내장 함수 자동 계산, 동적 배열 함수에 `_xlfn.` 자동 접두사), [시트](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sheet) (visible/hidden/veryHidden, 인쇄 여백, printTitleRows/Cols, RTL `sheetView`, 캐스케이드 인식 시트 이름 변경), [테이블](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-table), [정렬](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sort) (시트/범위, 다중 키, 사이드카 인식), [조건부 서식](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-conditionalformatting), [차트](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-chart) (상자 수염, [파레토](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-chart-add) 자동 정렬 + 누적%, 로그 축 포함), [피벗 테이블](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-pivottable) (다중 필드, 날짜 그룹화, showDataAs, 정렬, 총합계, 부분합, 압축/개요/표 형식 레이아웃, 항목 레이블 반복, 빈 행, 계산 필드), [슬라이서](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-slicer), [이름 범위](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-namedrange), [데이터 유효성 검사](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-validation), [이미지](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-picture) (PNG/JPG/GIF/SVG, 이중 표현 폴백), [스파크라인](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sparkline), [메모](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-comment) (RTL), [자동 필터](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-autofilter), [도형](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-shape), [OLE 객체](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-ole), CSV/TSV 가져오기, `$Sheet:A1` 셀 주소 지정 + +**PowerPoint** — [슬라이드](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide) (머리글/바닥글/날짜/슬라이드 번호 토글, 숨김), [도형](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-shape) (패턴 채우기, 흐림 효과, 하이퍼링크 툴팁 + 슬라이드 점프 링크), [이미지](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-picture) (PNG/JPG/GIF/SVG, 채우기 모드: stretch/contain/cover/tile, 밝기/대비/광선/그림자), [표](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-table), [차트](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-chart), [애니메이션](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide), [모프 전환](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-morph-check), [3D 모델 (.glb)](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-3dmodel), [슬라이드 줌](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-zoom), [수식](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-equation), [테마](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-theme), [연결선](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-connector), [비디오/오디오](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-video), [그룹](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-group), [노트](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-notes) (RTL, lang), [메모](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-comment) (RTL), [OLE 객체](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-ole), [플레이스홀더](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-placeholder) (phType로 추가/설정) + +## 사용 사례 + +**개발자용:** +- 데이터베이스나 API에서 보고서 자동 생성 +- 문서 일괄 처리(일괄 검색/교체, 스타일 업데이트) +- CI/CD 환경에서 문서 파이프라인 구축(테스트 결과에서 문서 생성) +- Docker/컨테이너 환경에서의 헤드리스 Office 자동화 + +**AI 에이전트용:** +- 사용자 프롬프트에서 프레젠테이션 생성(위 예시 참조) +- 문서에서 구조화된 데이터를 JSON으로 추출 +- 납품 전 문서 품질 검증 + +**팀용:** +- 문서 템플릿을 복제하고 데이터 입력 +- CI/CD 파이프라인에서 자동 문서 검증 + +## 설치 + +단일 자체 완결형 바이너리로 제공. .NET 런타임 내장 -- 설치할 것도, 관리할 런타임도 없습니다. + +**원라인 설치:** + +```bash +# macOS / Linux +curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash + +# Windows (PowerShell) +irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex +``` + +**또는 수동 다운로드** [GitHub Releases](https://github.com/iOfficeAI/OfficeCLI/releases): + +| 플랫폼 | 바이너리 | +|--------|---------| +| macOS Apple Silicon | `officecli-mac-arm64` | +| macOS Intel | `officecli-mac-x64` | +| Linux x64 | `officecli-linux-x64` | +| Linux ARM64 | `officecli-linux-arm64` | +| Windows x64 | `officecli-win-x64.exe` | +| Windows ARM64 | `officecli-win-arm64.exe` | + +설치 확인: `officecli --version` + +**또는 다운로드한 바이너리에서 셀프 설치 (`officecli`를 직접 실행해도 설치가 트리거됩니다):** + +```bash +officecli install # 명시적 설치 +officecli # 직접 실행으로도 설치 트리거 +``` + +업데이트는 백그라운드에서 자동 확인됩니다. `officecli config autoUpdate false`로 비활성화하거나 `OFFICECLI_SKIP_UPDATE=1`로 단일 실행 시 건너뛸 수 있습니다. 설정은 `~/.officecli/config.json`에 있습니다. + +## 주요 기능 + +### 내장 엔진과 생성 프리미티브 + +OfficeCLI는 자체 포함입니다. 아래 기능은 모두 바이너리 내장 — **Office 불필요**. + +#### 렌더링 엔진 + +처음부터 구현한 에이전트 친화적 렌더링 엔진이 바이너리 자체에 포함되어, 도형, 차트 (추세선, 오차 막대, 워터폴, 캔들스틱, 스파크라인), 수식 (OMML → MathJax 호환), Three.js로 렌더링되는 3D `.glb` 모델, 모프 전환, 슬라이드 줌, 도형 효과를 커버합니다. 페이지별 PNG 스크린샷은 렌더링된 HTML을 헤드리스 브라우저로 캡처해 생성됩니다. 세 가지 모드: + +- **`view html`** — 독립형 HTML 파일, 에셋 인라인. 모든 브라우저에서 열 수 있습니다. +- **`view screenshot`** — 페이지별 PNG, 멀티모달 에이전트용. +- **`watch`** — 로컬 HTTP 서버 + 자동 새로고침 미리보기. `add` / `set` / `remove`마다 브라우저 즉시 업데이트. Excel watch는 인라인 셀 편집과 차트 드래그 재배치 지원. + +```bash +officecli view deck.pptx html -o /tmp/deck.html +officecli view deck.pptx screenshot -o /tmp/deck.png # 여러 페이지는 --page 1-N +officecli watch deck.pptx # http://localhost:26315 +``` + +> 시각화 없이는 슬라이드를 생성하는 에이전트는 눈먼 채로 비행하는 것과 같습니다 — DOM은 읽을 수 있지만 제목이 넘쳤는지, 두 도형이 겹쳤는지는 판단할 수 없습니다. 렌더링이 바이너리에 내장되어 있어 "렌더링 → 보기 → 수정" 루프는 CI, Docker, 디스플레이 없는 서버 — 바이너리가 실행되는 어디서나 작동합니다. + +#### 수식 & 피벗 엔진 + +150+ Excel 함수가 작성 시 자동 평가 — `=SUM(A1:A2)`를 작성하고, 셀을 `get` 하면, 값이 이미 거기. Office에서 재계산하는 라운드트립 불필요. 동적 배열 함수 (`FILTER` / `UNIQUE` / `SORT` / `SEQUENCE`, `_xlfn.` 자동 접두사), `VLOOKUP` / `INDEX` / `MATCH`, 날짜 & 텍스트 함수 등 140+ 함수 커버. + +또한 소스 범위에서 단일 명령으로 네이티브 OOXML 피벗 테이블 — 멀티 필드 행/열/필터, 10가지 집계, `showDataAs` 모드, 날짜 그룹화, 계산 필드, Top-N, 레이아웃. 피벗 캐시 + 정의가 OOXML에 기록되어 Excel은 집계가 채워진 상태로 파일을 엽니다: + +```bash +officecli add sales.xlsx '/Sheet1' --type pivottable \ + --prop source='Data!A1:E10000' --prop rows='Region,Category' \ + --prop cols=Quarter --prop values='Revenue:sum,Units:avg' \ + --prop showDataAs=percentOfTotal +``` + +#### 템플릿 병합 — 한 번 설계, N번 채우기 + +`merge`는 모든 `.docx` / `.xlsx` / `.pptx`의 `{{key}}` 자리표시자를 JSON 데이터로 교체 — 단락, 표 셀, 도형, 머리글/바닥글, 차트 제목 전체에서 작동. 에이전트가 한 번 레이아웃을 설계 (비싸다), 프로덕션 코드가 N번 채운다 (싸고, 결정론적, 토큰 비용 제로). 에이전트가 각 보고서를 처음부터 재생성하여 N개의 일관성 없는 레이아웃을 만드는 실패 모드를 피합니다. + +```bash +officecli merge invoice-template.docx out-001.docx '{"client":"Acme","total":"$5,200"}' +officecli merge q4-template.pptx q4-acme.pptx data.json +``` + +#### Dump 라운드트립 — 기존 문서에서 학습 + +`dump`는 모든 `.docx`를 — 전체 문서 **또는 임의의 서브트리** (단일 단락, 표, styles, numbering, theme, settings) — 재생 가능한 batch JSON으로 직렬화하고, `batch`가 재생합니다. 사용자가 모방하고 싶은 샘플 문서가 주어지면, 에이전트는 원시 OOXML XML이 아닌 구조화된 사양을 읽고, 변경하여 재생합니다. "기존 템플릿이 있다"와 "100개 변형을 생성해 줘" 사이의 다리. + +```bash +officecli dump existing.docx -o blueprint.json # 전체 문서 +officecli dump existing.docx /body/tbl[1] -o table.json # 임의의 서브트리 +officecli batch new.docx --input blueprint.json +``` + +### 레지던트 모드와 배치 + +다단계 워크플로우에서 레지던트 모드는 문서를 메모리에 유지합니다. 배치 모드는 한 번의 open/save 사이클에서 여러 작업을 실행합니다. + +```bash +# 레지던트 모드 — 명명된 파이프로 거의 제로 지연 +officecli open report.docx +officecli set report.docx /body/p[1]/r[1] --prop bold=true +officecli set report.docx /body/p[2]/r[1] --prop color=FF0000 +officecli close report.docx + +# 배치 모드 — 원자적 다중 명령 실행 (기본적으로 첫 오류에서 중지) +echo '[{"command":"set","path":"/slide[1]/shape[1]","props":{"text":"Hello"}}, + {"command":"set","path":"/slide[1]/shape[2]","props":{"fill":"FF0000"}}]' \ + | officecli batch deck.pptx --json + +# 인라인 배치 — stdin 불필요 +officecli batch deck.pptx --commands '[{"op":"set","path":"/slide[1]/shape[1]","props":{"text":"Hi"}}]' + +# --force로 오류를 건너뛰고 계속 실행 +officecli batch deck.pptx --input updates.json --force --json +``` + +### 3계층 아키텍처 + +간단하게 시작하고, 필요할 때만 깊이 들어가세요. + +| 레이어 | 용도 | 명령어 | +|--------|------|--------| +| **L1: 읽기** | 콘텐츠의 시맨틱 뷰 | `view` (text, annotated, outline, stats, issues, html, svg, screenshot) | +| **L2: DOM** | 구조화된 요소 작업 | `get`, `query`, `set`, `add`, `remove`, `move`, `swap` | +| **L3: 원시 XML** | XPath 직접 접근 — 범용 폴백 | `raw`, `raw-set`, `add-part`, `validate` | + +```bash +# L1 — 고수준 뷰 +officecli view report.docx annotated +officecli view budget.xlsx text --cols A,B,C --max-lines 50 + +# L2 — 요소 수준 작업 +officecli query report.docx "run:contains(TODO)" +officecli add budget.xlsx / --type sheet --prop name="Q2 Report" +officecli move report.docx /body/p[5] --to /body --index 1 + +# L3 — L2로 부족할 때 원시 XML +officecli raw deck.pptx '/slide[1]' +officecli raw-set report.docx document \ + --xpath "//w:p[1]" --action append \ + --xml 'Injected text' +``` + +## AI 통합 + +### MCP 서버 + +내장 [MCP](https://modelcontextprotocol.io) 서버 — 명령어 하나로 등록: + +```bash +officecli mcp claude # Claude Code +officecli mcp cursor # Cursor +officecli mcp vscode # VS Code / Copilot +officecli mcp lmstudio # LM Studio +officecli mcp list # 등록 상태 확인 +``` + +JSON-RPC로 모든 문서 작업을 제공 — 셸 접근 불필요. + +### 직접 CLI 통합 + +2단계로 OfficeCLI를 모든 AI 에이전트에 통합: + +1. **바이너리 설치** -- 명령어 하나 ([설치](#설치) 참조) +2. **완료.** OfficeCLI가 AI 도구(Claude Code, GitHub Copilot, Codex)를 자동 감지하고, 알려진 설정 디렉토리를 확인하여 스킬 파일을 설치합니다. 에이전트는 즉시 Office 문서를 생성, 읽기, 수정할 수 있습니다. + +
+수동 설정 (선택사항) + +자동 설치가 환경을 지원하지 않는 경우, 스킬 파일을 수동으로 설치할 수 있습니다: + +**SKILL.md를 에이전트에 직접 제공:** + +```bash +curl -fsSL https://officecli.ai/SKILL.md +``` + +**Claude Code 로컬 스킬로 설치:** + +```bash +curl -fsSL https://officecli.ai/SKILL.md -o ~/.claude/skills/officecli.md +``` + +**기타 에이전트:** `SKILL.md`의 내용을 에이전트의 시스템 프롬프트 또는 도구 설명에 포함하세요. + +
+ +### 에이전트가 OfficeCLI에서 잘 동작하는 이유 + +- **결정론적 JSON 출력** — 모든 명령이 `--json`을 지원하며 스키마가 일관됩니다. 정규표현식 파싱 불필요, stdout 스크래핑 불필요. +- **경로 기반 주소 지정** — 모든 요소에 안정적인 경로 (`/slide[1]/shape[2]`). 에이전트는 XML 네임스페이스를 이해하지 않고도 문서를 탐색합니다. (OfficeCLI 자체 구문: 1-based 인덱스, 요소 로컬 이름 — XPath 아님.) +- **점진적 복잡도 (L1 → L2 → L3)** — 에이전트는 읽기 전용 뷰부터 시작해, DOM 작업으로 에스컬레이트, 필요할 때만 raw XML로 폴백. 토큰 사용을 최소화. +- **자가 치유 워크플로우** — `validate`, `view issues`, 그리고 구조화된 에러 코드 (`not_found`, `invalid_value`, `unsupported_property`) 가 suggestion과 유효 범위를 반환합니다. 에이전트는 사람의 개입 없이 자가 수정. +- **내장 에이전트 친화적 렌더링 엔진** — `view html` / `view screenshot` / `watch`가 네이티브로 HTML과 PNG를 출력. Office 불필요. 에이전트는 CI / Docker / 헤드리스 환경에서도 자신의 출력을 "보고" 레이아웃 문제를 수정할 수 있습니다. +- **내장 수식 & 피벗 엔진** — 150+ Excel 함수 작성 시 자동 평가; 소스 범위에서 단일 명령으로 네이티브 OOXML 피벗 테이블. 에이전트는 Office에서 재계산할 필요 없이 계산값과 집계 결과를 즉시 읽습니다. +- **템플릿 병합** — 에이전트가 한 번 레이아웃을 설계, 다운스트림 코드가 `{{key}}` 자리표시자를 N번 채움. 각 보고서를 재생성하며 토큰을 태우는 것을 방지. +- **라운드트립 Dump** — `dump`가 모든 `.docx`를 재생 가능한 batch JSON으로. 에이전트는 raw OOXML XML이 아닌 구조화된 사양을 읽어 인간이 작성한 샘플에서 학습. +- **내장 도움말** — 속성명이나 값 형식이 헷갈릴 때, 에이전트는 추측하지 않고 `officecli set `를 실행. +- **자동 설치** — OfficeCLI는 AI 도구 (Claude Code, Cursor, VS Code…) 를 감지하고 자가 구성합니다. 수동 skill 파일 설정 불필요. + +### 내장 도움말 + +속성 이름을 모를 때, 계층형 도움말로 확인: + +```bash +officecli pptx set # 모든 설정 가능한 요소와 속성 +officecli pptx set shape # 특정 요소 유형의 세부사항 +officecli pptx set shape.fill # 단일 속성 형식과 예시 +officecli docx query # 셀렉터 참조: 속성, :contains, :has() 등 +``` + +`pptx`를 `docx`나 `xlsx`로 대체 가능. 동사는 `view`, `get`, `query`, `set`, `add`, `raw`. + +`officecli --help`로 전체 개요 확인. + +### JSON 출력 스키마 + +모든 명령어가 `--json`을 지원합니다. 일반적인 응답 형식: + +**단일 요소** (`get --json`): + +```json +{"tag": "shape", "path": "/slide[1]/shape[1]", "attributes": {"name": "TextBox 1", "text": "Hello"}} +``` + +**요소 목록** (`query --json`): + +```json +[ + {"tag": "paragraph", "path": "/body/p[1]", "attributes": {"style": "Heading1", "text": "Title"}}, + {"tag": "paragraph", "path": "/body/p[5]", "attributes": {"style": "Heading1", "text": "Summary"}} +] +``` + +**오류**는 구조화된 오류 객체를 반환합니다. 오류 코드, 수정 제안, 사용 가능한 값을 포함: + +```json +{ + "success": false, + "error": { + "error": "Slide 50 not found (total: 8)", + "code": "not_found", + "suggestion": "Valid Slide index range: 1-8" + } +} +``` + +오류 코드: `not_found`, `invalid_value`, `unsupported_property`, `invalid_path`, `unsupported_type`, `missing_property`, `file_not_found`, `file_locked`, `invalid_selector`. 속성 이름은 자동 교정 지원 -- 속성 이름 오타 시 가장 근접한 매칭을 제안합니다. + +**오류 복구** -- 에이전트가 사용 가능한 요소를 확인하여 자체 수정: + +```bash +# 에이전트가 잘못된 경로 시도 +officecli get report.docx /body/p[99] --json +# 반환: {"success": false, "error": {"error": "...", "code": "not_found", "suggestion": "..."}} + +# 에이전트가 사용 가능한 요소를 확인하여 자체 수정 +officecli get report.docx /body --depth 1 --json +# 사용 가능한 하위 요소 목록 반환, 에이전트가 올바른 경로 선택 +``` + +**변경 확인** (`set`, `add`, `remove`, `move`, `create`에서 `--json` 사용 시): + +```json +{"success": true, "path": "/slide[1]/shape[1]"} +``` + +`officecli --help`로 종료 코드와 오류 형식의 전체 설명 확인. + +## 비교 + +| | OfficeCLI | Microsoft Office | LibreOffice | python-docx / openpyxl | +|---|---|---|---|---| +| 오픈소스 & 무료 | ✓ (Apache 2.0) | ✗ (유료 라이선스) | ✓ | ✓ | +| AI 네이티브 CLI + JSON | ✓ | ✗ | ✗ | ✗ | +| 제로 설치 (단일 바이너리) | ✓ | ✗ | ✗ | ✗ (Python + pip 필요) | +| 모든 언어에서 호출 | ✓ (CLI) | ✗ (COM/Add-in) | ✗ (UNO API) | Python만 | +| 경로 기반 요소 접근 | ✓ | ✗ | ✗ | ✗ | +| 원시 XML 폴백 | ✓ | ✗ | ✗ | 부분 지원 | +| 내장 에이전트 친화적 렌더링 엔진 | ✓ | ✗ | ✗ | ✗ | +| 헤드리스 HTML/PNG 출력 | ✓ | ✗ | 부분 지원 | ✗ | +| 크로스 포맷 템플릿 병합 (`{{key}}`) | ✓ | ✗ | ✗ | ✗ | +| Dump → batch JSON 라운드트립 | ✓ | ✗ | ✗ | ✗ | +| 라이브 미리보기 (편집 후 자동 새로고침) | ✓ | ✗ | ✗ | ✗ | +| 헤드리스 / CI | ✓ | ✗ | 부분 지원 | ✓ | +| 크로스 플랫폼 | ✓ | Windows/Mac | ✓ | ✓ | +| Word + Excel + PowerPoint | ✓ | ✓ | ✓ | 여러 라이브러리 필요 | + +## 명령어 참조 + +| 명령어 | 설명 | +|--------|------| +| [`create`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-create) | 빈 .docx, .xlsx, .pptx 생성 (확장자로 유형 결정) | +| [`view`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-view) | 콘텐츠 보기 (모드: `outline`, `text`, `annotated`, `stats`, `issues`, `html`) | +| [`get`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-get) | 요소와 하위 요소 가져오기 (`--depth N`, `--json`) | +| [`query`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-query) | CSS 스타일 쿼리 (`[attr=value]`, `:contains()`, `:has()` 등) | +| [`set`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-set) | 요소 속성 수정 | +| [`add`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-add) | 요소 추가 (또는 `--from `로 복제) | +| [`remove`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-remove) | 요소 삭제 | +| [`move`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-move) | 요소 이동 (`--to `, `--index N`, `--after `, `--before `) | +| [`swap`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-swap) | 두 요소 교체 | +| [`validate`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-validate) | OpenXML 스키마 검증 | +| [`batch`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-batch) | 한 번의 open/save 사이클에서 여러 작업 실행 (stdin, `--input`, 또는 `--commands`; 기본적으로 첫 오류에서 중지, `--force`로 계속) | +| [`merge`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-merge) | 템플릿 병합 — `{{key}}` 플레이스홀더를 JSON 데이터로 교체 | +| [`watch`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-watch) | 브라우저에서 라이브 HTML 미리보기, 자동 새로고침 | +| [`mcp`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-mcp) | AI 도구 통합용 MCP 서버 시작 | +| [`raw`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-raw) | 문서 파트의 원시 XML 보기 | +| [`raw-set`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-raw) | XPath로 원시 XML 수정 | +| `add-part` | 새 문서 파트 추가 (머리글, 차트 등) | +| [`open`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-open) | 레지던트 모드 시작 (문서를 메모리에 유지) | +| `close` | 저장하고 레지던트 모드 종료 | +| [`install`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-install) | 바이너리 + 스킬 + MCP 설치 (`all`, `claude`, `cursor` 등) | +| `config` | 설정 가져오기 또는 변경 | +| ` ` | [내장 도움말](https://github.com/iOfficeAI/OfficeCLI/wiki/command-reference) (예: `officecli pptx set shape`) | + +## 엔드투엔드 워크플로우 예시 + +전형적인 에이전트 자가 치유 워크플로우: 프레젠테이션 생성, 콘텐츠 입력, 검증, 문제 수정 -- 모두 사람의 개입 없이. + +```bash +# 1. 생성 +officecli create report.pptx + +# 2. 콘텐츠 추가 +officecli add report.pptx / --type slide --prop title="Q4 Results" +officecli add report.pptx '/slide[1]' --type shape \ + --prop text="Revenue: $4.2M" --prop x=2cm --prop y=5cm --prop size=28 +officecli add report.pptx / --type slide --prop title="Details" +officecli add report.pptx '/slide[2]' --type shape \ + --prop text="Growth driven by new markets" --prop x=2cm --prop y=5cm + +# 3. 검증 +officecli view report.pptx outline +officecli validate report.pptx + +# 4. 문제 수정 +officecli view report.pptx issues --json +# 출력에 따라 문제 수정: +officecli set report.pptx '/slide[1]/shape[1]' --prop font=Arial +``` + +### 단위와 색상 + +모든 치수 및 색상 속성은 유연한 입력 형식을 지원: + +| 유형 | 지원 형식 | 예시 | +|------|----------|------| +| **치수** | cm, in, pt, px 또는 원시 EMU | `2cm`, `1in`, `72pt`, `96px`, `914400` | +| **색상** | 16진수, 색상 이름, RGB, 테마 색상 | `#FF0000`, `FF0000`, `red`, `rgb(255,0,0)`, `accent1` | +| **글꼴 크기** | 숫자만 또는 pt 접미사 | `14`, `14pt`, `10.5pt` | +| **간격** | pt, cm, in 또는 배율 | `12pt`, `0.5cm`, `1.5x`, `150%` | + +## 자주 사용하는 패턴 + +```bash +# Word 문서의 모든 Heading1 텍스트 교체 +officecli query report.docx "paragraph[style=Heading1]" --json | ... +officecli set report.docx /body/p[1]/r[1] --prop text="New Title" + +# 모든 슬라이드 콘텐츠를 JSON으로 내보내기 +officecli get deck.pptx / --depth 2 --json + +# Excel 셀 일괄 업데이트 +officecli batch budget.xlsx --input updates.json --json + +# CSV 데이터를 Excel 시트로 가져오기 +officecli add budget.xlsx / --type sheet --prop name="Q1 Data" --prop csv=sales.csv + +# 템플릿 병합으로 보고서 일괄 생성 +officecli merge invoice-template.docx invoice-001.docx '{"client":"Acme","total":"$5,200"}' + +# 납품 전 문서 품질 확인 +officecli validate report.docx && officecli view report.docx issues --json +``` + +**Python에서 호출** — 한 번 래핑하면 모든 호출이 파싱된 JSON을 반환합니다: + +```python +import json, subprocess + +def cli(*args): + return json.loads(subprocess.check_output(["officecli", *args, "--json"], text=True)) + +cli("create", "deck.pptx") +cli("add", "deck.pptx", "/", "--type", "slide", "--prop", "title=Q4 보고서") +slide = cli("get", "deck.pptx", "/slide[1]") +print(slide["attributes"]["text"]) +``` + +## 문서 + +[Wiki](https://github.com/iOfficeAI/OfficeCLI/wiki)에서 모든 명령어, 요소 유형, 속성의 상세 가이드를 확인하세요: + +- **형식별:** [Word](https://github.com/iOfficeAI/OfficeCLI/wiki/word-reference) | [Excel](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-reference) | [PowerPoint](https://github.com/iOfficeAI/OfficeCLI/wiki/powerpoint-reference) +- **워크플로우:** [엔드투엔드 예시](https://github.com/iOfficeAI/OfficeCLI/wiki/workflows) -- Word 보고서, Excel 대시보드, PPT 프레젠테이션, 일괄 수정, 레지던트 모드 +- **문제 해결:** [자주 발생하는 오류와 해결책](https://github.com/iOfficeAI/OfficeCLI/wiki/troubleshooting) +- **AI 에이전트 가이드:** [Wiki 내비게이션 결정 트리](https://github.com/iOfficeAI/OfficeCLI/wiki/agent-guide) + +## 소스에서 빌드 + +컴파일에는 [.NET 10 SDK](https://dotnet.microsoft.com/download)가 필요합니다. 출력은 자체 완결형 네이티브 바이너리 -- .NET이 내장되어 있어 실행 시 설치 불필요. + +```bash +./build.sh +``` + +## 라이선스 + +[Apache License 2.0](LICENSE) + +버그 리포트와 기여는 [GitHub Issues](https://github.com/iOfficeAI/OfficeCLI/issues)로 환영합니다. + +--- + +OfficeCLI가 유용하다면 [GitHub에서 스타를 눌러주세요](https://github.com/iOfficeAI/OfficeCLI) — 더 많은 사람들이 프로젝트를 발견하는 데 도움이 됩니다. + +[OfficeCLI.AI](https://OfficeCLI.AI) | [GitHub](https://github.com/iOfficeAI/OfficeCLI) + + + + diff --git a/README_zh.md b/README_zh.md index 1b6df7edf..a6e33ee80 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,18 +1,32 @@ # OfficeCLI +> **这是 [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI) 的一个 fork**,由 [@NextDoorLaoHuang-HF](https://github.com/NextDoorLaoHuang-HF) 维护,为 [AionUI](https://github.com/iOfficeAI/AionUi) 预览体验优化。 +> +> **相对于上游的差异:** +> - **HTML 预览:修订模式(Track Changes)渲染** — 插入/删除/格式/移动标记在 `view html` 和 Watch 模式中以独立 CSS 样式可视化渲染 +> - **Watch 服务:修订 API 端点** — `POST /api/revision/accept`、`POST /api/revision/reject`、`GET /api/revision/count`,支持浏览器内接受/拒绝修订 +> - **修订工具栏** — Watch HTML 中注入浮动工具栏,显示修订计数徽章和接受/拒绝按钮 +> +> 所有改动干净地 rebase 在上游 `main` 之上(领先 3 个提交)。构建方式不变:`dotnet publish -c Release -r osx-arm64 --self-contained -o out`。 > **OfficeCLI 是全球首个、也是最好的专为 AI 智能体设计的 Office 套件。** -**让任何 AI 智能体完全掌控 Word、Excel 和 PowerPoint -- 只需一行代码。** +**让任何 AI 智能体完全掌控 Word、Excel 和 PowerPoint——只需一行代码。** 开源免费。单一可执行文件。无需安装 Office。零依赖。全平台运行。 +**内置 agent 友好渲染引擎** —— 智能体可以"看见"自己创建的内容,无需 Office。把 `.docx` / `.xlsx` / `.pptx` 渲染为 HTML 或 PNG,"渲染 → 看 → 改" 循环在二进制能跑的任何地方都成立。 + [![GitHub Release](https://img.shields.io/github/v/release/iOfficeAI/OfficeCLI)](https://github.com/iOfficeAI/OfficeCLI/releases) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) -[English](README.md) | **中文** +[English](README.md) | **中文** | [日本語](README_ja.md) | [한국어](README_ko.md) + +

+ 💬 社区: Discord +

- 在 AionUi 上使用 OfficeCLI 的 PPT 制作过程 + 在 AionUi 上使用 OfficeCLI 的 PPT 制作过程

AionUi 上使用 OfficeCLI 的 PPT 制作过程

@@ -66,38 +80,66 @@ curl -fsSL https://officecli.ai/SKILL.md 就这一步。技能文件会教智能体如何安装二进制文件并使用所有命令。 -> **技术细节:** OfficeCLI 附带 [SKILL.md](SKILL.md)(239 行,约 8K tokens),涵盖命令语法、架构设计和常见陷阱。安装后,您的智能体可以立即创建、读取和修改任何 Office 文档。 +## 普通用户 -## 快速开始 +**方式 A — 图形界面:** 安装 [**AionUi**](https://github.com/iOfficeAI/AionUi) — 一款桌面应用,用自然语言就能创建和编辑 Office 文档,底层由 OfficeCLI 驱动。只需描述你想要什么,AionUi 帮你搞定。 -从零到完成一个演示文稿,只需几秒钟: +**方式 B — 命令行:** 从源码构建(见下方[安装](#安装)),然后运行: ```bash -# 创建新的 PowerPoint -officecli create deck.pptx +officecli install +``` -# 添加带标题和背景色的幻灯片 -officecli add deck.pptx / --type slide --prop title="Q4 Report" --prop background=1A1A2E +该命令会将二进制文件复制到 PATH,并自动将 **officecli 技能文件**安装到检测到的所有 AI 编程助手 — Claude Code、Cursor、Windsurf、GitHub Copilot 等。您的智能体可以立即创建、读取和编辑 Office 文档,无需额外配置。 -# 在幻灯片上添加文本形状 -officecli add deck.pptx /slide[1] --type shape \ - --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm \ - --prop font=Arial --prop size=24 --prop color=FFFFFF +**AionUI 用户** — 一条命令启用修订模式(Track Changes): -# 查看演示文稿大纲 -officecli view deck.pptx outline +```bash +officecli setup-aionui ``` -输出: +这会安装 `officecli-track-changes` 技能并在 AionUI 中注册 **Word 修订助手**。重启 AionUI 后即可在助手列表中看到 — 适用于合同审查、修订标记等工作流。 -``` -Slide 1: Q4 Report - Shape 1 [TextBox]: Revenue grew 25% +## 开发者 — 30 秒亲眼看到效果 + +```bash +# 1. 构建并安装(macOS / Linux) +git clone https://github.com/NextDoorLaoHuang-HF/OfficeCLI.git && cd OfficeCLI +dotnet publish src/officecli/officecli.csproj -c Release -r osx-arm64 --self-contained -o out +sudo cp out/officecli /usr/local/bin/ + +# 2. 创建一个空白 PowerPoint +officecli create deck.pptx + +# 3. 启动实时预览 — 浏览器自动打开 http://localhost:26315 +officecli watch deck.pptx + +# 4. 打开另一个终端,添加一页幻灯片 — 浏览器即时刷新 +officecli add deck.pptx / --type slide --prop title="Hello, World!" ``` +就这么简单。你执行的每一条 `add`、`set`、`remove` 命令都会实时刷新预览。继续尝试吧 — 浏览器就是你的实时反馈窗口。 + +## 快速开始 + ```bash +# 创建演示文稿并添加内容 +officecli create deck.pptx +officecli add deck.pptx / --type slide --prop title="Q4 Report" --prop background=1A1A2E +officecli add deck.pptx '/slide[1]' --type shape \ + --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm \ + --prop font=Arial --prop size=24 --prop color=FFFFFF + +# 查看大纲 +officecli view deck.pptx outline +# → Slide 1: Q4 Report +# → Shape 1 [TextBox]: Revenue grew 25% + +# 查看 HTML — 在浏览器中打开渲染预览,无需启动服务器 +officecli view deck.pptx html + # 获取任意元素的结构化 JSON -officecli get deck.pptx /slide[1]/shape[1] --json +officecli get deck.pptx '/slide[1]/shape[1]' --json ``` ```json @@ -148,11 +190,11 @@ officecli add deck.pptx / --type slide --prop title="Q4 Report" | Excel (.xlsx) | ✅ | ✅ | ✅ | | PowerPoint (.pptx) | ✅ | ✅ | ✅ | -**Word** — [段落](https://github.com/iOfficeAI/OfficeCLI/wiki/word-paragraph)、[文本片段](https://github.com/iOfficeAI/OfficeCLI/wiki/word-run)、[表格](https://github.com/iOfficeAI/OfficeCLI/wiki/word-table)、[样式](https://github.com/iOfficeAI/OfficeCLI/wiki/word-style)、[页眉/页脚](https://github.com/iOfficeAI/OfficeCLI/wiki/word-header-footer)、[图片](https://github.com/iOfficeAI/OfficeCLI/wiki/word-picture)、[公式](https://github.com/iOfficeAI/OfficeCLI/wiki/word-equation)、[批注](https://github.com/iOfficeAI/OfficeCLI/wiki/word-comment)、[脚注](https://github.com/iOfficeAI/OfficeCLI/wiki/word-footnote)、[水印](https://github.com/iOfficeAI/OfficeCLI/wiki/word-watermark)、[书签](https://github.com/iOfficeAI/OfficeCLI/wiki/word-bookmark)、[目录](https://github.com/iOfficeAI/OfficeCLI/wiki/word-toc)、[图表](https://github.com/iOfficeAI/OfficeCLI/wiki/word-chart)、[超链接](https://github.com/iOfficeAI/OfficeCLI/wiki/word-hyperlink)、[节](https://github.com/iOfficeAI/OfficeCLI/wiki/word-section)、[表单域](https://github.com/iOfficeAI/OfficeCLI/wiki/word-formfield)、[内容控件 (SDT)](https://github.com/iOfficeAI/OfficeCLI/wiki/word-sdt)、[域](https://github.com/iOfficeAI/OfficeCLI/wiki/word-field)、[文档属性](https://github.com/iOfficeAI/OfficeCLI/wiki/word-document) +**Word** — 完整的 [i18n 与 RTL 支持](https://github.com/iOfficeAI/OfficeCLI/wiki/i18n)(按脚本字体槽位、按脚本 BCP-47 语言标签 `lang.latin/ea/cs`、复杂脚本粗体/斜体/字号、`direction=rtl` 在段落/文本片段/节/表格/样式/页眉/页脚/docDefaults 间级联、`rtlGutter` + `pgBorders` 简写、印地语/阿拉伯语/泰语/中日韩本地化页码)、[段落](https://github.com/iOfficeAI/OfficeCLI/wiki/word-paragraph)、[文本片段](https://github.com/iOfficeAI/OfficeCLI/wiki/word-run)、[表格](https://github.com/iOfficeAI/OfficeCLI/wiki/word-table)、[样式](https://github.com/iOfficeAI/OfficeCLI/wiki/word-style)、[页眉/页脚](https://github.com/iOfficeAI/OfficeCLI/wiki/word-header-footer)、[图片](https://github.com/iOfficeAI/OfficeCLI/wiki/word-picture)(PNG/JPG/GIF/SVG)、[公式](https://github.com/iOfficeAI/OfficeCLI/wiki/word-equation)、[批注](https://github.com/iOfficeAI/OfficeCLI/wiki/word-comment)、[脚注](https://github.com/iOfficeAI/OfficeCLI/wiki/word-footnote)、[水印](https://github.com/iOfficeAI/OfficeCLI/wiki/word-watermark)、[书签](https://github.com/iOfficeAI/OfficeCLI/wiki/word-bookmark)、[目录](https://github.com/iOfficeAI/OfficeCLI/wiki/word-toc)、[图表](https://github.com/iOfficeAI/OfficeCLI/wiki/word-chart)、[超链接](https://github.com/iOfficeAI/OfficeCLI/wiki/word-hyperlink)、[节](https://github.com/iOfficeAI/OfficeCLI/wiki/word-section)、[表单域](https://github.com/iOfficeAI/OfficeCLI/wiki/word-formfield)、[内容控件 (SDT)](https://github.com/iOfficeAI/OfficeCLI/wiki/word-sdt)、[域](https://github.com/iOfficeAI/OfficeCLI/wiki/word-field)(22 种零参数 + MERGEFIELD / REF / PAGEREF / SEQ / STYLEREF / DOCPROPERTY / IF)、[OLE 对象](https://github.com/iOfficeAI/OfficeCLI/wiki/word-ole)、[文档属性](https://github.com/iOfficeAI/OfficeCLI/wiki/word-document) -**Excel** — [单元格](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-cell)、公式(内置 150+ 函数自动求值)、[工作表](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sheet)、[表格](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-table)、[条件格式](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-conditionalformatting)、[图表](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-chart)、[数据透视表](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-pivottable)、[命名范围](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-namedrange)、[数据验证](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-validation)、[图片](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-picture)、[迷你图](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sparkline)、[批注](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-comment)、[自动筛选](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-autofilter)、[形状](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-shape)、CSV/TSV 导入、`$Sheet:A1` 单元格寻址 +**Excel** — [单元格](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-cell)(添加时支持音标/振假名)、公式(内置 150+ 函数自动求值,动态数组函数自动加 `_xlfn.` 前缀)、[工作表](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sheet)(visible/hidden/veryHidden、打印边距、printTitleRows/Cols、RTL `sheetView`、级联感知的工作表重命名)、[表格](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-table)、[排序](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sort)(工作表/区域、多键、附属感知)、[条件格式](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-conditionalformatting)、[图表](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-chart)(含箱线图、[帕累托图](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-chart-add) 自动排序 + 累计百分比、对数轴)、[数据透视表](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-pivottable)(多字段、日期分组、showDataAs、排序、总计、分类汇总、紧凑/大纲/表格布局、重复项目标签、空白行、计算字段)、[切片器](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-slicer)、[命名范围](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-namedrange)、[数据验证](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-validation)、[图片](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-picture)(PNG/JPG/GIF/SVG,双重表示回退)、[迷你图](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-sparkline)、[批注](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-comment)(RTL)、[自动筛选](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-autofilter)、[形状](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-shape)、[OLE 对象](https://github.com/iOfficeAI/OfficeCLI/wiki/excel-ole)、CSV/TSV 导入、`$Sheet:A1` 单元格寻址 -**PowerPoint** — [幻灯片](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide)、[形状](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-shape)、[图片](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-picture)、[表格](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-table)、[图表](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-chart)、[动画](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide)、[morph 过渡](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-morph-check)、[3D 模型(.glb)](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-3dmodel)、[幻灯片缩放](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-zoom)、[公式](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-equation)、[主题](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-theme)、[连接线](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-connector)、[视频/音频](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-video)、[组合](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-group)、[备注](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-notes)、[占位符](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-placeholder) +**PowerPoint** — [幻灯片](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide)(页眉/页脚/日期/页码切换、隐藏)、[形状](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-shape)(图案填充、模糊效果、超链接提示 + 跳转幻灯片链接)、[图片](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-picture)(PNG/JPG/GIF/SVG,填充模式:stretch/contain/cover/tile,亮度/对比度/发光/阴影)、[表格](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-table)、[图表](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-chart)、[动画](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-slide)、[morph 过渡](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-morph-check)、[3D 模型(.glb)](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-3dmodel)、[幻灯片缩放](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-zoom)、[公式](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-equation)、[主题](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-theme)、[连接线](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-connector)、[视频/音频](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-video)、[组合](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-group)、[备注](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-notes)(RTL、lang)、[批注](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-comment)(RTL)、[OLE 对象](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-ole)、[占位符](https://github.com/iOfficeAI/OfficeCLI/wiki/ppt-placeholder)(按 phType 添加/设置) ## 使用场景 @@ -173,51 +215,77 @@ officecli add deck.pptx / --type slide --prop title="Q4 Report" ## 安装 -单一自包含可执行文件,.NET 运行时已内嵌 -- 无需安装任何依赖,无需管理运行时。 +本 fork 不发布独立 release。请从源码构建: -**一键安装:** +**前置条件:** +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) ```bash -# macOS / Linux -curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash - -# Windows (PowerShell) -irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex +git clone https://github.com/NextDoorLaoHuang-HF/OfficeCLI.git +cd OfficeCLI +dotnet publish src/officecli/officecli.csproj -c Release -r osx-arm64 --self-contained -o out +sudo cp out/officecli /usr/local/bin/ ``` -**或手动下载** [GitHub Releases](https://github.com/iOfficeAI/OfficeCLI/releases): - -| 平台 | 文件名 | -|------|--------| -| macOS Apple Silicon | `officecli-mac-arm64` | -| macOS Intel | `officecli-mac-x64` | -| Linux x64 | `officecli-linux-x64` | -| Linux ARM64 | `officecli-linux-arm64` | -| Windows x64 | `officecli-win-x64.exe` | -| Windows ARM64 | `officecli-win-arm64.exe` | +**其他平台:** 将 `osx-arm64` 替换为 `osx-x64`、`linux-x64`、`linux-arm64`、`win-x64` 或 `win-arm64`。 验证安装:`officecli --version` -**或从已下载的二进制文件自安装:** +> **提示:** 如需上游原始 release(含预编译二进制),请见 [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI#安装)。 + +## 核心功能 + +### 内置引擎与生成原语 + +OfficeCLI 是自包含的。下列能力全部内置在二进制中——**无需 Office**。 + +#### 渲染引擎 + +从零实现的 agent 友好渲染引擎内置在二进制中,覆盖形状、图表(趋势线、误差线、瀑布、K 线、sparkline)、公式(OMML → MathJax 兼容)、通过 Three.js 渲染的 3D `.glb` 模型、morph 过渡、幻灯片缩放、形状效果。按页 PNG 截图是把渲染出的 HTML 通过无头浏览器截出来的。三种模式: + +- **`view html`** —— 独立 HTML 文件,资源内联。任何浏览器打开即可看。 +- **`view screenshot`** —— 按页 PNG,供多模态智能体读图检查。 +- **`watch`** —— 本地 HTTP 服务 + 自动刷新预览;每次 `add` / `set` / `remove` 立即更新浏览器。Excel watch 还支持单元格内联编辑、图表拖动定位。 ```bash -officecli install +officecli view deck.pptx html -o /tmp/deck.html +officecli view deck.pptx screenshot -o /tmp/deck.png # 多页用 --page 1-N +officecli watch deck.pptx # http://localhost:26315 ``` -OfficeCLI 会在后台自动检查更新。通过 `officecli config autoUpdate false` 关闭,或通过 `OFFICECLI_SKIP_UPDATE=1` 跳过单次检查。配置文件位于 `~/.officecli/config.json`。 +> 没有可视化,生成 PPT 的智能体就是在盲跑——它能读 DOM,但分辨不出标题溢出、两个形状重叠。因为渲染引擎内置在二进制里,"渲染 → 看 → 改"循环在 CI、Docker、无显示器的服务器——只要二进制能跑的地方都能用。 -## 核心功能 +#### 公式与透视引擎 -### 实时预览 +150+ Excel 函数写入即自动求值——写 `=SUM(A1:A2)`,`get` 单元格,值已经在那。不需要回到 Office 重算。覆盖动态数组函数(`FILTER` / `UNIQUE` / `SORT` / `SEQUENCE`,`_xlfn.` 自动加前缀)、`VLOOKUP` / `INDEX` / `MATCH`、日期与文本函数等。 -`watch` 启动本地 HTTP 服务器,实时预览 PowerPoint 文件。每次修改自动刷新浏览器 — 非常适合与 AI 智能体配合做迭代设计。 +外加从源数据范围一条命令生成原生 OOXML 数据透视表——多字段行/列/筛选器、10 种聚合方式、`showDataAs` 多种模式、日期分组、计算字段、Top-N、布局选项。透视表缓存和定义都写入 OOXML,Excel 打开即看到聚合后的结果: ```bash -officecli watch deck.pptx -# 打开 http://localhost:18080 — 每次 set/add/remove 自动刷新 +officecli add sales.xlsx '/Sheet1' --type pivottable \ + --prop source='Data!A1:E10000' --prop rows='Region,Category' \ + --prop cols=Quarter --prop values='Revenue:sum,Units:avg' \ + --prop showDataAs=percentOfTotal ``` -支持形状、图表、公式、3D 模型(Three.js)、morph 过渡、缩放导航和所有形状效果的渲染。 +#### 模板合并 —— 设计一次,填充 N 次 + +`merge` 把任意 `.docx` / `.xlsx` / `.pptx` 中的 `{{key}}` 占位符替换为 JSON 数据——段落、表格单元格、形状、页眉页脚、图表标题都支持。智能体一次性设计版式(昂贵),生产代码填充 N 次(廉价、确定、零 token 成本)。避免了"每份报告都从头重生成、产出 N 份版式不一致"的失败模式。 + +```bash +officecli merge invoice-template.docx out-001.docx '{"client":"Acme","total":"$5,200"}' +officecli merge q4-template.pptx q4-acme.pptx data.json +``` + +#### Dump 往返 —— 从现有文档学习 + +`dump` 把任意 `.docx` —— 整个文档**或任意子树**(单段、单表、styles、numbering、theme、settings)——序列化为可重放的 batch JSON,`batch` 重放回去。给一份用户想模仿的范本,智能体读结构化规格而不是原始 OOXML XML,修改后重放。打通"我有一份现成模板"和"给我生成 100 份变体"之间的链路。 + +```bash +officecli dump existing.docx -o blueprint.json # 整个文档 +officecli dump existing.docx /body/tbl[1] -o table.json # 任意子树 +officecli batch new.docx --input blueprint.json +``` ### 驻留模式与批量执行 @@ -230,10 +298,16 @@ officecli set report.docx /body/p[1]/r[1] --prop bold=true officecli set report.docx /body/p[2]/r[1] --prop color=FF0000 officecli close report.docx -# 批量模式 — 原子化多命令执行 +# 批量模式 — 原子化多命令执行(默认遇到第一个错误即停止) echo '[{"command":"set","path":"/slide[1]/shape[1]","props":{"text":"Hello"}}, {"command":"set","path":"/slide[1]/shape[2]","props":{"fill":"FF0000"}}]' \ - | officecli batch deck.pptx --stop-on-error + | officecli batch deck.pptx --json + +# 内联 batch,无需标准输入 +officecli batch deck.pptx --commands '[{"op":"set","path":"/slide[1]/shape[1]","props":{"text":"Hi"}}]' + +# 使用 --force 跳过错误继续执行 +officecli batch deck.pptx --input updates.json --force --json ``` ### 三层架构 @@ -242,8 +316,8 @@ echo '[{"command":"set","path":"/slide[1]/shape[1]","props":{"text":"Hello"}}, | 层 | 用途 | 命令 | |----|------|------| -| **L1:读取** | 内容的语义视图 | `view`(text、annotated、outline、stats、issues、html) | -| **L2:DOM** | 结构化元素操作 | `get`、`query`、`set`、`add`、`remove`、`move` | +| **L1:读取** | 内容的语义视图 | `view`(text、annotated、outline、stats、issues、html、svg、screenshot) | +| **L2:DOM** | 结构化元素操作 | `get`、`query`、`set`、`add`、`remove`、`move`、`swap` | | **L3:原始 XML** | XPath 直接访问 — 通用兜底 | `raw`、`raw-set`、`add-part`、`validate` | ```bash @@ -257,7 +331,7 @@ officecli add budget.xlsx / --type sheet --prop name="Q2 Report" officecli move report.docx /body/p[5] --to /body --index 1 # L3 — L2 不够时用原始 XML -officecli raw deck.pptx /slide[1] +officecli raw deck.pptx '/slide[1]' officecli raw-set report.docx document \ --xpath "//w:p[1]" --action append \ --xml 'Injected text' @@ -303,37 +377,22 @@ curl -fsSL https://officecli.ai/SKILL.md curl -fsSL https://officecli.ai/SKILL.md -o ~/.claude/skills/officecli.md ``` -**其他智能体:** 将 `SKILL.md`(239 行,约 8K tokens)的内容添加到智能体的系统提示词或工具描述中。 +**其他智能体:** 将 `SKILL.md` 的内容添加到智能体的系统提示词或工具描述中。 -**从任意语言调用:** - -```python -# Python -import subprocess, json -def cli(*args): return subprocess.check_output(["officecli", *args], text=True) -cli("create", "deck.pptx") -cli("set", "deck.pptx", "/slide[1]/shape[1]", "--prop", "text=Hello") -``` - -```js -// JavaScript -const { execFileSync } = require('child_process') -const cli = (...args) => execFileSync('officecli', args, { encoding: 'utf8' }) -cli('set', 'deck.pptx', '/slide[1]/shape[1]', '--prop', 'text=Hello') -``` - -每个命令都支持 `--json` 输出结构化数据。基于路径的寻址让智能体无需理解 XML 命名空间。 +### 智能体为什么在 OfficeCLI 上如鱼得水 -### 为什么智能体偏爱 OfficeCLI - -- **确定性 JSON 输出** -- 每个命令都支持 `--json`,返回结构一致的数据。无需正则解析。 -- **基于路径的寻址** -- 每个元素都有稳定的路径(`/slide[1]/shape[2]`)。智能体无需理解 XML 命名空间即可导航文档。注意:路径使用 OfficeCLI 自有语法(1-based 索引,元素本地名称),非 XPath。 -- **渐进式复杂度** -- 从 L1(读取)开始,升级到 L2(修改),仅在必要时回退到 L3(原始 XML)。最大限度减少 token 消耗。 -- **自愈式工作流** -- `validate`、`view issues` 和帮助系统让智能体无需人工干预即可检测问题并自行修正。 -- **内置帮助** -- 属性名或取值格式不确定时,运行 `officecli set ` 即可查询,无需猜测。 -- **自动安装** -- 无需手动配置技能文件。OfficeCLI 自动检测您的 AI 工具并完成配置。 +- **确定性 JSON 输出** —— 每条命令都支持 `--json`,schema 一致。无需正则解析、无需抓 stdout。 +- **基于路径的寻址** —— 每个元素都有稳定路径(`/slide[1]/shape[2]`)。智能体无需理解 XML 命名空间即可导航文档。(OfficeCLI 自己的语法:1-based 索引、元素本地名——不是 XPath。) +- **渐进式复杂度(L1 → L2 → L3)** —— 智能体从只读视图入手,升级到 DOM 操作,仅在必要时降到 raw XML。最大限度节省 token。 +- **自愈式工作流** —— `validate`、`view issues`、以及结构化错误码(`not_found`、`invalid_value`、`unsupported_property`)会返回 suggestion 和有效范围。智能体无需人工介入即可自纠错。 +- **内置 agent 友好渲染引擎** —— `view html` / `view screenshot` / `watch` 原生输出 HTML 和 PNG。无需 Office。智能体能"看见"自己的产出,并在 CI / Docker / 无头环境里修复排版问题。 +- **内置公式与透视引擎** —— 150+ Excel 函数写入即自动求值;从源数据范围一条命令生成原生 OOXML 数据透视表。智能体立刻读到计算值和聚合结果,不需要回到 Office 重算。 +- **模板合并** —— 智能体一次性设计版式,下游代码把 `{{key}}` 占位符填充 N 次。避免每份报告都烧 token 重生成。 +- **Dump 往返** —— `dump` 把任意 `.docx` 转成可重放的 batch JSON。智能体通过读结构化规格学习人类范本,而不是从原始 OOXML XML 反推。 +- **内置帮助** —— 属性名或取值格式不确定时,智能体跑 `officecli set `,不靠猜。 +- **自动安装** —— OfficeCLI 自动识别您的 AI 工具(Claude Code、Cursor、VS Code…)并完成配置。无需手动放 skill 文件。 ### 内置帮助 @@ -414,18 +473,15 @@ officecli get report.docx /body --depth 1 --json | 任意语言调用 | ✓ (CLI) | ✗ (COM/Add-in) | ✗ (UNO API) | 仅 Python | | 基于路径的元素访问 | ✓ | ✗ | ✗ | ✗ | | 原始 XML 兜底 | ✓ | ✗ | ✗ | 部分支持 | -| 实时预览 | ✓ | ✓ | ✗ | ✗ | +| 内置 agent 友好渲染引擎 | ✓ | ✗ | ✗ | ✗ | +| 无头 HTML/PNG 输出 | ✓ | ✗ | 部分支持 | ✗ | +| 跨格式模板合并(`{{key}}`)| ✓ | ✗ | ✗ | ✗ | +| Dump → batch JSON 往返 | ✓ | ✗ | ✗ | ✗ | +| 实时预览(编辑后自动刷新) | ✓ | ✗ | ✗ | ✗ | | 无头 / CI 环境 | ✓ | ✗ | 部分支持 | ✓ | | 跨平台 | ✓ | Windows/Mac | ✓ | ✓ | | Word + Excel + PowerPoint | ✓ | ✓ | ✓ | 需要多个库 | -## 更新与配置 - -```bash -officecli config autoUpdate false # 关闭自动更新检查 -OFFICECLI_SKIP_UPDATE=1 officecli ... # 单次调用跳过检查(CI) -``` - ## 命令参考 | 命令 | 说明 | @@ -437,10 +493,10 @@ OFFICECLI_SKIP_UPDATE=1 officecli ... # 单次调用跳过检查(CI | [`set`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-set) | 修改元素属性 | | [`add`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-add) | 添加元素(或通过 `--from ` 克隆) | | [`remove`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-remove) | 删除元素 | -| [`move`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-move) | 移动元素(`--to --index N`) | +| [`move`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-move) | 移动元素(`--to `、`--index N`、`--after `、`--before `) | | [`swap`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-swap) | 交换两个元素 | | [`validate`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-validate) | OpenXML 模式校验 | -| [`batch`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-batch) | 单次打开/保存周期内执行多条操作(JSON 通过标准输入或 `--input`) | +| [`batch`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-batch) | 单次打开/保存周期内执行多条操作(stdin、`--input` 或 `--commands`;默认遇到第一个错误停止,`--force` 跳过错误继续) | | [`merge`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-merge) | 模板合并 — 用 JSON 数据替换 `{{key}}` 占位符 | | [`watch`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-watch) | 在浏览器中实时 HTML 预览,自动刷新 | | [`mcp`](https://github.com/iOfficeAI/OfficeCLI/wiki/command-mcp) | 启动 MCP 服务器,用于 AI 工具集成 | @@ -463,10 +519,10 @@ officecli create report.pptx # 2. 添加内容 officecli add report.pptx / --type slide --prop title="Q4 Results" -officecli add report.pptx /slide[1] --type shape \ +officecli add report.pptx '/slide[1]' --type shape \ --prop text="Revenue: $4.2M" --prop x=2cm --prop y=5cm --prop size=28 officecli add report.pptx / --type slide --prop title="Details" -officecli add report.pptx /slide[2] --type shape \ +officecli add report.pptx '/slide[2]' --type shape \ --prop text="Growth driven by new markets" --prop x=2cm --prop y=5cm # 3. 验证 @@ -476,22 +532,7 @@ officecli validate report.pptx # 4. 修复发现的问题 officecli view report.pptx issues --json # 根据输出修复问题,例如: -officecli set report.pptx /slide[1]/shape[1] --prop font=Arial -``` - -### 模板合并 - -用 JSON 数据替换文档中的 `{{key}}` 占位符 -- 支持段落、表格单元格、形状、页眉页脚、图表标题等所有文本内容。 - -```bash -# 内联 JSON 数据 -officecli merge template.docx output.docx '{"name":"Alice","dept":"Sales","date":"2026-03-30"}' - -# 从 JSON 文件读取数据 -officecli merge template.pptx report.pptx data.json - -# Excel 模板 -officecli merge budget-template.xlsx q4-budget.xlsx '{"quarter":"Q4","year":"2026"}' +officecli set report.pptx '/slide[1]/shape[1]' --prop font=Arial ``` ### 单位与颜色 @@ -528,6 +569,20 @@ officecli merge invoice-template.docx invoice-001.docx '{"client":"Acme","total" officecli validate report.docx && officecli view report.docx issues --json ``` +**Python 调用** —— 包装一次,每次调用都返回解析好的 JSON: + +```python +import json, subprocess + +def cli(*args): + return json.loads(subprocess.check_output(["officecli", *args, "--json"], text=True)) + +cli("create", "deck.pptx") +cli("add", "deck.pptx", "/", "--type", "slide", "--prop", "title=Q4 报告") +slide = cli("get", "deck.pptx", "/slide[1]") +print(slide["attributes"]["text"]) +``` + ## 文档 [Wiki](https://github.com/iOfficeAI/OfficeCLI/wiki) 提供了每个命令、元素类型和属性的详细指南: @@ -553,6 +608,8 @@ officecli validate report.docx && officecli view report.docx issues --json --- +如果觉得 OfficeCLI 好用,请在 [GitHub 上点个 Star](https://github.com/iOfficeAI/OfficeCLI) — 帮助更多人发现这个项目。 + [OfficeCLI.AI](https://OfficeCLI.AI) | [GitHub](https://github.com/iOfficeAI/OfficeCLI) @@ -585,7 +641,6 @@ keywords: office, cli, ai-agent, automation, docx, xlsx, pptx, openxml, document ai-agent-compatible: true mcp-server: true skill-file: SKILL.md -skill-file-lines: 239 alternatives: python-docx, openpyxl, python-pptx, libreoffice --headless install-command-unix: curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash install-command-windows: irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex diff --git a/SKILL.md b/SKILL.md index d64ff8732..6715ed754 100644 --- a/SKILL.md +++ b/SKILL.md @@ -7,23 +7,19 @@ description: Create, analyze, proofread, and modify Office documents (.docx, .xl AI-friendly CLI for .docx, .xlsx, .pptx. Single binary, no dependencies, no Office installation needed. -## Install & Update +## Install -Same command for both install and upgrade: +If `officecli` is not installed: ```bash # macOS / Linux -curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash +curl -fsSL https://d.officecli.ai/install.sh | bash # Windows (PowerShell) -irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex +irm https://d.officecli.ai/install.ps1 | iex ``` -After installation, run `source ~/.zshrc` (macOS) or `source ~/.bashrc` (Linux) to make the `officecli` command available. - -Verify: `officecli --version` - -officecli auto-updates daily in the background. +Verify with `officecli --version`. If still not found after install, open a new terminal. --- @@ -31,32 +27,39 @@ officecli auto-updates daily in the background. **L1 (read) → L2 (DOM edit) → L3 (raw XML)**. Always prefer higher layers. Add `--json` for structured output. +**Before doc work, check Specialized Skills** (bottom of this file). Fundraising decks, academic papers, financial models, dashboards, and Morph animations need their own skill loaded first — `load_skill` once, then proceed. + --- ## Help System (IMPORTANT) -**When unsure about property names, value formats, or command syntax, ALWAYS run help instead of guessing.** One help query is faster than guess-fail-retry loops. +**When unsure about property names, value formats, or command syntax, ALWAYS run help instead of guessing.** One help query beats guess-fail-retry loops. + +`officecli help` ≡ `officecli --help`, and `officecli --help` ≡ `officecli help ` — same content. -**Three-layer navigation** — start from the deepest level you know: ```bash -officecli pptx set # All settable elements and their properties -officecli pptx set shape # Shape properties in detail -officecli pptx set shape.fill # Specific property format and examples +officecli help # All commands + global options + schema entry points +officecli help docx # List all docx elements +officecli help docx paragraph # Full schema: properties, aliases, examples, readbacks +officecli help docx set paragraph # Verb-filtered: only props usable with `set` +officecli help docx paragraph --json # Structured schema (machine-readable) ``` -Replace `pptx` with `docx` or `xlsx`. Commands: `view`, `get`, `query`, `set`, `add`, `raw`. +Format aliases: `word`→`docx`, `excel`→`xlsx`, `ppt`/`powerpoint`→`pptx`. Verbs: `add`, `set`, `get`, `query`, `remove`. MCP exposes the same schema via `{"command":"help","format":"docx","type":"paragraph"}`. --- ## Performance: Resident Mode -For multi-step workflows (3+ commands on the same file), use `open`/`close`: +**Every command auto-starts a resident on first access** (60s idle timeout) — file-lock conflicts are automatically avoided. Explicit `open`/`close` is still recommended for longer sessions (12min idle): ```bash -officecli open report.docx # keep in memory — fast subsequent commands +officecli open report.docx # explicitly keep in memory officecli set report.docx ... # no file I/O overhead officecli close report.docx # save and release ``` +Opt out of auto-start: `OFFICECLI_NO_AUTO_RESIDENT=1`. + --- ## Quick Start @@ -65,8 +68,7 @@ officecli close report.docx # save and release ```bash officecli create slides.pptx officecli add slides.pptx / --type slide --prop title="Q4 Report" --prop background=1A1A2E -officecli add slides.pptx /slide[1] --type shape --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm --prop font=Arial --prop size=24 --prop color=FFFFFF -officecli set slides.pptx /slide[1] --prop transition=fade --prop advanceTime=3000 +officecli add slides.pptx '/slide[1]' --type shape --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm --prop font=Arial --prop size=24 --prop color=FFFFFF ``` **Word:** @@ -80,9 +82,7 @@ officecli add report.docx /body --type paragraph --prop text="Revenue increased ```bash officecli create data.xlsx officecli set data.xlsx /Sheet1/A1 --prop value="Name" --prop bold=true -officecli set data.xlsx /Sheet1/B1 --prop value="Score" --prop bold=true officecli set data.xlsx /Sheet1/A2 --prop value="Alice" -officecli set data.xlsx /Sheet1/B2 --prop value=95 ``` --- @@ -91,7 +91,7 @@ officecli set data.xlsx /Sheet1/B2 --prop value=95 ```bash officecli create # Create blank .docx/.xlsx/.pptx (type from extension) -officecli view # outline | stats | issues | text | annotated +officecli view # outline | stats | issues | text | annotated | html officecli get --depth N # Get a node and its children [--json] officecli query # CSS-like query officecli validate # Validate against OpenXML schema @@ -106,10 +106,14 @@ officecli validate # Validate against OpenXML schema | `issues` | Formatting/content/structure problems | `--type format\|content\|structure`, `--limit N` | | `text` | Plain text extraction | `--start N --end N`, `--max-lines N` | | `annotated` | Text with formatting annotations | | +| `html` | Static HTML snapshot — same renderer as `watch`, no server needed | `--browser`, `--page N` (docx), `--start N --end N` (pptx) | +| `screenshot` / `svg` / `pdf` / `forms` | PNG via headless browser / SVG (pptx slide) / PDF via exporter plugin / form-fields JSON via format-handler plugin | `-o`, `--screenshot-width/-height`, pptx `--grid N` | + +Use `view html` for one-shot snapshots (CI artifacts, archival, diffing); use `watch` when you need live refresh or browser-side click-to-select. ### get -Any XML path via element localName. Use `--depth N` to expand children. Add `--json` for structured output. +Any XML path via element localName. Use `--depth N` to expand children. Add `--json` for structured output. Default text output is grep-friendly: `path (type) "text" key=val key=val ...` ```bash officecli get report.docx '/body/p[3]' --depth 2 --json @@ -117,7 +121,18 @@ officecli get slides.pptx '/slide[1]' --depth 1 # list all shapes on sl officecli get data.xlsx '/Sheet1/B2' --json ``` -Run `officecli docx get` / `officecli xlsx get` / `officecli pptx get` for all available paths. +### Stable ID Addressing + +Elements with stable IDs return `@attr=value` paths instead of positional indices. Prefer these in multi-step workflows — positional indices shift on insert/delete, stable IDs do not. + +``` +/slide[1]/shape[@id=550950021] # PPT shape +/slide[1]/table[@id=1388430425]/tr[1]/tc[2] # PPT table +/body/p[@paraId=1A2B3C4D] # Word paragraph +/comments/comment[@commentId=1] # Word comment +``` + +PPT also accepts `@name=` (e.g. `shape[@name=Title 1]`), with morph `!!` prefix awareness. Elements without stable IDs (slide, run, tr/tc, row) fall back to positional indices. ### query @@ -128,14 +143,53 @@ officecli query report.docx 'paragraph[style=Normal] > run[font!=Arial]' officecli query slides.pptx 'shape[fill=FF0000]' ``` -### validate +--- + +## Watch & Interactive Selection + +Live HTML preview that auto-refreshes on every file change. Browsers can click / shift-click / box-drag to select shapes; the CLI can read the current browser selection and act on it. + +```bash +officecli watch [--port N] # Start preview server (default port 26315) +officecli unwatch # Stop +officecli goto # Scroll watching browser(s) to element (docx: p / table / tr / tc) +``` + +Open the printed `http://localhost:N` URL. Click to select; shift/cmd/ctrl+click to multi-select; drag from empty space to box-select. PPT/Word use blue outline; Excel uses native-style green selection (double-click cell to edit inline; drag a chart to reposition). + +### `get selected` — read what the user clicked + +```bash +officecli get selected [--json] +``` + +Returns DocumentNodes for whatever is currently selected. Empty result if nothing selected. Exit code != 0 if no watch is running. ```bash -officecli validate report.docx # Check for schema errors -officecli validate slides.pptx # Must pass before delivery +# User clicks shapes in the browser, then asks "make these red" +PATHS=$(officecli get deck.pptx selected --json | jq -r '.data.Results[].path') +for p in $PATHS; do officecli set deck.pptx "$p" --prop fill=FF0000; done ``` -**For large documents**, ALWAYS use `--max-lines` or `--start`/`--end` to limit output. +### Key properties + +- **Selection survives file edits.** Paths use stable `@id=` form. +- **All connected browsers share one selection.** Last-write-wins. +- **Same-file single-watch.** A given file can have only one watch process at a time. +- **Group shapes select as a whole.** Drilling into individual children of a group is not supported in v1. +- **Coverage:** `.pptx` shapes/pictures/tables/charts/connectors/groups; `.docx` top-level paragraphs and tables. Inherited layout/master decorations and Word nested elements (table cells, run-level) are not addressable. **`.xlsx` does not emit `data-path`** — `mark`/`selection` on xlsx always resolve `stale=true` (v2 candidate). + +### Marks — edit proposals waiting for review + +Use `mark` when changes need human review BEFORE they hit the file. Marks live in the watch process only; a separate `set` pipeline applies accepted ones. For one-shot changes use `set` directly; for permanent file annotations use `add --type comment` (Word native). + +```bash +officecli mark [--prop find=... color=... note=... tofix=... regex=true] [--json] +officecli unmark [--path

| --all] [--json] +officecli get-marks [--json] +``` + +Props: `find` (literal or regex when `regex=true`; raw form `find='r"[abc]"'`), `color` (hex / `rgb(...)` / 22 named whitelist), `note`, `tofix` (drives apply pipeline). **Path** must be `data-path` format from watch HTML — see subskills for full pipeline. --- @@ -147,57 +201,138 @@ officecli validate slides.pptx # Must pass before delivery officecli set --prop key=value [--prop ...] ``` -**Any XML attribute is settable** via element path (found via `get --depth N`) — even attributes not currently present. - -Run `officecli set` for all settable elements. Run `officecli set ` for detail. +**Any XML attribute is settable** via element path (found via `get --depth N`) — even attributes not currently present. Without `find=`, `set` applies format to the entire element. **Value formats:** | Type | Format | Examples | |------|--------|---------| -| Colors | Hex, named, RGB, theme | `FF0000`, `red`, `rgb(255,0,0)`, `accent1`..`accent6` | +| Colors | Hex (with/without `#`), named, RGB, theme | `FF0000`, `#FF0000`, `red`, `rgb(255,0,0)`, `accent1`..`accent6` | | Spacing | Unit-qualified | `12pt`, `0.5cm`, `1.5x`, `150%` | | Dimensions | EMU or suffixed | `914400`, `2.54cm`, `1in`, `72pt`, `96px` | +**Dotted-attr aliases** — `font.` forms accepted on shape/run/paragraph/table/row/cell/section/styles, e.g. `--prop font.color=red --prop font.bold=true --prop font.size=14pt`. Run `officecli help ` for the full list. + +### find — format or replace matched text + +Use `find=` with `set` to target specific text for formatting or replacement. Format props are separate `--prop` flags — do NOT nest them. + +```bash +# Format matched text (auto-splits runs) +officecli set doc.docx '/body/p[1]' --prop find=weather --prop bold=true --prop color=red + +# Regex matching +officecli set doc.docx '/body/p[1]' --prop 'find=\d+%' --prop regex=true --prop color=red + +# Replace text (use `/` for whole-document scope) +officecli set doc.docx / --prop find=draft --prop replace=final + +# PPT — same syntax, different paths +officecli set slides.pptx / --prop find=draft --prop replace=final +``` + +**Path controls search scope:** `/` = whole document, `/body/p[1]` or `/slide[N]/shape[M]` = specific element, `/header[1]` / `/footer[1]` = headers/footers. + +**Notes:** +- Case-sensitive by default. Case-insensitive: `--prop 'find=(?i)error' --prop regex=true` +- Matches work across run boundaries +- No match = silent success. `--json` includes `"matched": N` +- **Excel:** only `find` + `replace` supported (no find + format props) + ### add — add elements or clone ```bash -officecli add --type [--index N] [--prop ...] -officecli add --from [--index N] # clone existing element +officecli add --type [--prop ...] +officecli add --type --after [--prop ...] # insert after anchor +officecli add --type --before [--prop ...] # insert before anchor +officecli add --type --index N [--prop ...] # 0-based position (legacy) +officecli add --from # clone existing element ``` +`--after`, `--before`, `--index` are mutually exclusive. No position flag = append to end. + **Element types (with aliases):** | Format | Types | |--------|-------| -| **pptx** | slide, shape (textbox), picture (image/img), chart, table, row (tr), connector (connection/line), group, video (audio/media), equation (formula/math), notes, paragraph (para), run, zoom (slidezoom) | -| **docx** | paragraph (para), run, table, row (tr), cell (td), image (picture/img), header, footer, section, bookmark, comment, footnote, endnote, formfield, sdt (contentcontrol), chart, equation (formula/math), field, hyperlink, style, toc, watermark, break (pagebreak/columnbreak) | -| **xlsx** | sheet, row, cell, chart, image (picture), comment, table (listobject), namedrange (definedname), pivottable (pivot), sparkline, validation (datavalidation), autofilter, shape, textbox, databar/colorscale/iconset/formulacf (conditional formatting), csv (tsv) | +| **pptx** | slide (incl. hidden), shape (font.latin/ea/cs, direction=rtl, underline.color, effective.X+effective.X.src; arrow alias for rightArrow; slideMaster/slideLayout typed add/set/remove), picture (SVG, brightness/contrast/glow/shadow, rotation, link, tooltip), chart (direction=rtl, pieOfPie, barOfPie, axisLine/gridline per-attr setters, animation+chartBuild=byCategory|bySeries, line dropLines/hiLowLines/upDownBars, anchor=x,y,w,h shorthand), table (cell direction=rtl, fill/background, built-in PowerPoint style catalogue, /col[C] get + swap/copyFrom, row/col Move/CopyFrom), row (tr), connector (from/to accept @name=, startshape/endshape SetByPath), group (link, tooltip, deep walk by get/query/add/remove), video/audio (loop, autoStart alias), equation, notes (direction=rtl, lang), comment (legacy + modern p188 threaded round-trip), animation (15 emphasis + 16 exit presets, multi-effect chains, motion-path presets, repeat/restart/autoReverse, chart animations), transition (12 p15 presets + morph/p14), paragraph (para), run, zoom, ole (preview=, full dump round-trip via add-part+raw-set), placeholder (phType=...), model3d (rotation=ax,ay,az; full dump round-trip), smartart (dump round-trip via add-part). | +| **docx** | paragraph (direction/font.latin/ea/cs, bold.cs/italic.cs/size.cs, lang.latin/ea/cs, wordWrap, framePr.\*, tabs shorthand), run (lang slots, direction, underline.color, position half-pts, **revision.type=ins\|del\|format\|moveFrom\|moveTo + revision.action=accept\|reject** with .author/.date — `/revision[@author=X]` selector for filtered accept/reject), table (direction=rtl, hMerge, **virtual column ops**: add/remove/move/copyfrom on /body/tbl[N]/col), row (tr), cell (td), image, header/footer (direction), section (pageNumFmt full enum, direction=rtl, rtlGutter, pgBorders=box), bookmark, comment, footnote, endnote, formfield, sdt, chart, equation, field (28 types), hyperlink, style (direction, indents, pbdr, lineSpacing on Add/Set), toc, watermark, break, ole, **num/abstractNum/lvl**, **tab**, **textbox/shape** (full Add+Get; geometry, fill, line, wrap, alt, anchor). docDefaults.rtl, autoHyphenation, `get /` exposes locale + /comments /footnotes /endnotes. `create --minimal` for raw OOXML scaffolding. | +| **xlsx** | sheet (visible/hidden/veryHidden, print margins, printTitleRows/Cols, rightToLeft sheetView, cascade-aware rename), row (c{N}= cell-content shorthand; add accepts --from /Sheet/col[L]; formula-ref rewrite on insert), col (formula-ref rewrite, named-range follow on move), cell (type=richtext+runs, merge=range/sweep, direction=rtl, phonetic; **--shift left\|up on remove, shift=right\|down on add** — Excel UI dialog parity; formula auto-detect; OFFSET/INDIRECT in calc), chart (per-axis RTL/title, anchor=x,y,w,h, pareto), image (SVG), comment (direction=rtl), table (listobject), namedrange (definedname, volatile, `[@name=X]`; formula-body inlined at parse), pivottable (cache CoW + cross-pivot sharing, labelFilter, topN, fillDownLabels, calculatedField), sparkline, validation, autofilter, shape, textbox, CF (databar/colorscale/iconset/formulacf/cellIs/topN/aboveAverage), ole, csv. Query supports `merge`/`mergedrange`. Workbook: password. Shape selector enumerates leaves inside grpSp. | + +### Pivot tables (xlsx) + +```bash +officecli add data.xlsx /Sheet1 --type pivottable \ + --prop source="Sheet1!A1:E100" --prop rows=Region,Category \ + --prop cols=Year --prop values="Sales:sum,Qty:count" \ + --prop grandTotals=rows --prop subtotals=off --prop sort=asc +``` + +Key props: `rows`, `cols`, `values` (Field:func[:showDataAs]), `filters`, `source`, `position`, `layout` (compact/outline/tabular), `repeatLabels`, `blankRows`, `aggregate`, `showDataAs` (percent_of_total/row/col, running_total), `grandTotals`, `subtotals`, `sort`. Aggregators: sum, count, average, max, min, product, stdDev, stdDevp, var, varp, countNums. Date columns auto-group. Run `officecli help xlsx pivottable` for full schema. + +### Document-level properties (all formats) + +```bash +officecli set doc.docx / --prop docDefaults.font=Arial --prop docDefaults.fontSize=11pt +officecli set doc.docx / --prop protection=forms --prop evenAndOddHeaders=true +officecli set data.xlsx / --prop calc.mode=manual --prop calc.refMode=r1c1 +officecli set slides.pptx / --prop defaultFont=Arial --prop show.loop=true --prop print.what=handouts +``` + +Run `officecli help /` for all document-level properties (docDefaults, docGrid, CJK spacing, calc, print, show, theme, extended). + +### Sort (xlsx) + +```bash +officecli set data.xlsx /Sheet1 --prop sort="C desc" --prop sortHeader=true +officecli set data.xlsx '/Sheet1/A1:D100' --prop sort="A asc" --prop sortHeader=true +``` -**Clone:** `officecli add / --from /slide[1]` — copies with all cross-part relationships. +Format: `COL DIR[, COL DIR ...]`. Rejects ranges with merged cells or formulas. Sidecar metadata (hyperlinks, comments, conditional formatting, drawings) follows rows automatically. -Run `officecli add` for all addable types and their properties. +### Text-anchored insert (`--after find:X` / `--before find:X`) + +Locate an insertion point by text match within a paragraph. Inline types (run, picture, hyperlink) insert within the paragraph; block types (table, paragraph) auto-split it. PPT only supports inline. + +```bash +# Word: inline run after matched text +officecli add doc.docx '/body/p[1]' --type run --after find:weather --prop text=" (sunny)" + +# Word: block table after matched text (auto-splits paragraph) +officecli add doc.docx '/body/p[1]' --type table --after "find:First sentence." --prop rows=2 --prop cols=2 +``` + +### Clone + +`officecli add / --from '/slide[1]'` — copies with all cross-part relationships. ### move, swap, remove ```bash -officecli move [--to ] [--index N] +officecli move [--to ] [--index N] [--after ] [--before ] officecli swap officecli remove '/body/p[4]' ``` +When using `--after` or `--before`, `--to` can be omitted — the target container is inferred from the anchor. + ### batch — multiple operations in one save cycle +Continues on error by default (returns exit 1 if any item fails). Use `--stop-on-error` to abort on the first failure. `--force` is the docx-protection bypass. + +`officecli dump []` emits a replayable batch JSON for round-trip — `.docx` (full coverage) and `.pptx` (text/tables/pictures/charts/notes/theme + OLE/3D/video/audio/SmartArt/morph/p15 transitions via raw-set passthrough). Path defaults to `/` (whole document); pass a subtree path (`/body`, `/body/p[N]`, `/body/tbl[N]`, `/theme`, `/settings`, `/numbering`, `/styles`) to scope the dump. `officecli refresh ` recalculates TOC page numbers / PAGE / cross-references after replay (Word backend on Windows; headless-HTML fallback elsewhere). `officecli plugins list` extends support to `.doc`, `.hwpx`, `.pdf` export. + ```bash echo '[ {"command":"set","path":"/Sheet1/A1","props":{"value":"Name","bold":"true"}}, {"command":"set","path":"/Sheet1/B1","props":{"value":"Score","bold":"true"}} ]' | officecli batch data.xlsx --json -``` -Batch supports: `add`, `set`, `get`, `query`, `remove`, `move`, `view`, `raw`, `raw-set`, `validate`. +officecli batch data.xlsx --commands '[{"op":"set","path":"/Sheet1/A1","props":{"value":"Done"}}]' --json +officecli batch data.xlsx --input updates.json --force --json +``` -Batch fields: `command`, `path`, `parent`, `type`, `from`, `to`, `index`, `props` (dict), `selector`, `mode`, `depth`, `part`, `xpath`, `action`, `xml`. +Supports: `add`, `set`, `get`, `query`, `remove`, `move`, `swap`, `view`, `raw`, `raw-set`, `validate`. Fields: `command` (or `op`), `path`, `parent`, `type`, `from`, `to`, `index`, `after`, `before`, `props`, `selector`, `mode`, `depth`, `part`, `xpath`, `action`, `xml`. --- @@ -211,9 +346,7 @@ officecli raw-set --xpath "..." --action replace --xml '... # create new document part (returns rId) ``` -**raw-set actions:** `append`, `prepend`, `insertbefore`, `insertafter`, `replace`, `remove`, `setattr`. - -Run `officecli raw` for available parts per format. +`raw-set` actions: `append`, `prepend`, `insertbefore`, `insertafter`, `replace`, `remove`, `setattr`. Run `officecli help raw` for available parts. --- @@ -221,30 +354,52 @@ Run `officecli raw` for available parts per format. | Pitfall | Correct Approach | |---------|-----------------| -| `--name "foo"` | ❌ Use `--prop name="foo"` — all attributes go through `--prop` | -| `x=-3cm` | ❌ Negative coordinates not supported. Use `x=0cm` or `x=36cm` | -| `/shape[myname]` | ❌ Name indexing not supported. Use numeric index: `/shape[3]` | -| Guessing property names | ❌ Run `officecli set ` to see exact names | -| Modifying an open file | ❌ Close the file in PowerPoint/WPS first | -| `\n` in shell strings | ❌ Use `\\n` for newlines in `--prop text="..."` | +| `--name "foo"` | Use `--prop name="foo"` — all attributes go through `--prop` | +| Unquoted `[N]` paths in zsh/bash | Always quote: `'/slide[1]'` or `"/slide[1]"` (shell glob-expands brackets) | +| PPT `shape[1]` for content | `shape[1]` is typically the title placeholder. Use `shape[2]+` for content shapes | +| `/shape[myname]` | Name indexing not supported. Use numeric index or `@name=` (PPT only) | +| Guessing property names | Run `officecli help ` to see exact names | +| Modifying an open file | Close the file in PowerPoint/WPS first | +| `\n` in shell strings | Use `\\n` for newlines in `--prop text="..."` | +| `$` in shell text | `--prop text="$15M"` strips `$15`. Use single quotes: `--prop text='$15M'`, or heredoc batch | --- ## Specialized Skills -This skill covers the officecli CLI basics. For complex scenarios, load the dedicated skill for better results: +`officecli load_skill ` — output is a SKILL.md, follow its rules. + +**Loading rule**: +- Pick the most specific match in "When to use"; if none fits, load the format default (`word` / `pptx` / `excel`). +- Scenes already contain the format default's rules — load **one** skill per artifact, never stack. +- Loaded rules persist across turns; don't re-load each reply. +- Two distinct artifacts → two separate loads. + +### Word (.docx) + +| Name | When to use | +|------|-------------| +| `word` | Reports, letters, memos, proposals, generic documents | +| `academic-paper` | Journal / conference / thesis: APA / Chicago / IEEE / MLA citations, equations, SEQ + PAGEREF cross-refs, multi-column journal layout, bibliography. NOT for business reports or letters (route those to `word`) | + +### PowerPoint (.pptx) + +| Name | When to use | +|------|-------------| +| `pptx` | Generic decks: board reviews, sales decks, all-hands, product launches | +| `pitch-deck` | **Fundraising only** — seed / Series A-C / SAFE / convertible / strategic raise. NOT for sales / product / board decks (route those to `pptx`) | +| `morph-ppt` | Cinematic Morph-animated presentations. NOT for static decks (route those to `pptx`) | +| `morph-ppt-3d` | 3D Morph: GLB models, camera moves, depth. NOT for 2D-only Morph (route those to `morph-ppt`) | + +### Excel (.xlsx) -| Scenario | Skill | Min Version | When to Use | -|----------|-------|:-----------:|-------------| -| **Word documents** | `officecli-docx` | v1.0.23 | Create, read, edit .docx — reports, letters, memos, proposals | -| **Academic papers** | `officecli-academic-paper` | v1.0.24 | Research papers, white papers with TOC, equations, footnotes, bibliography | -| **Presentations** | `officecli-pptx` | v1.0.23 | Create, read, edit .pptx — general slide decks | -| **Pitch decks** | `officecli-pitch-deck` | v1.0.24 | Investor decks, product launches, sales decks with charts and stat callouts | -| **Morph PPT** | `morph-ppt` | v1.0.24 | Morph-animated cinematic presentations | -| **Excel** | `officecli-xlsx` | v1.0.23 | Create, read, edit .xlsx — financial models, trackers, formulas | -| **Data dashboards** | `officecli-data-dashboard` | v1.0.24 | CSV/tabular data → Excel dashboards with KPI cards, charts, sparklines | +| Name | When to use | +|------|-------------| +| `excel` | Generic workbooks, formulas, pivots, trackers | +| `financial-model` | Financial models, scenarios, projections. NOT for general data analysis (route those to `excel`) | +| `data-dashboard` | CSV/tabular data → KPI / analytics / executive dashboards with charts and sparklines. NOT for raw data tracking (route those to `excel`) | -> **How to load:** Ask your AI tool to enable the skill by name, or load the skill file from `skills//SKILL.md`. +Example: a fundraising deck task → `officecli load_skill pitch-deck` → use the printed rules. --- @@ -252,5 +407,6 @@ This skill covers the officecli CLI basics. For complex scenarios, load the dedi - Paths are **1-based** (XPath convention): `'/body/p[3]'` = third paragraph - `--index` is **0-based** (array convention): `--index 0` = first position +- **Excel exception**: for `add --type row` and `add --type col`, `--index N` is **1-based** (matches OOXML RowIndex / column letter index). `--index 5` inserts at row 5 / column 5. - After modifications, verify with `validate` and/or `view issues` -- **When unsure**, run `officecli [element[.property]]` instead of guessing +- **When unsure**, run `officecli help ` instead of guessing diff --git a/assets/blackhole.gif b/assets/blackhole.gif index 70c72e5ab..efc31078f 100644 Binary files a/assets/blackhole.gif and b/assets/blackhole.gif differ diff --git a/assets/cat.gif b/assets/cat.gif index 4ac0402c3..0eb9a48a7 100644 Binary files a/assets/cat.gif and b/assets/cat.gif differ diff --git a/assets/designwhatmovesyou.gif b/assets/designwhatmovesyou.gif index 46cf669b7..403b574ed 100644 Binary files a/assets/designwhatmovesyou.gif and b/assets/designwhatmovesyou.gif differ diff --git a/assets/efforless.gif b/assets/efforless.gif index c7e5a0dab..577c4503f 100644 Binary files a/assets/efforless.gif and b/assets/efforless.gif differ diff --git a/assets/first-ppt-aionui.gif b/assets/first-ppt-aionui.gif index 61d7bb229..20ecbb71a 100644 Binary files a/assets/first-ppt-aionui.gif and b/assets/first-ppt-aionui.gif differ diff --git a/assets/hero-en.png b/assets/hero-en.png index 081c9a21d..b9752a2f8 100644 Binary files a/assets/hero-en.png and b/assets/hero-en.png differ diff --git a/assets/hero-zh.png b/assets/hero-zh.png index d8a48cc85..62444e026 100644 Binary files a/assets/hero-zh.png and b/assets/hero-zh.png differ diff --git a/assets/horizon.gif b/assets/horizon.gif index 500974091..7bdd1e6e8 100644 Binary files a/assets/horizon.gif and b/assets/horizon.gif differ diff --git a/assets/mars.gif b/assets/mars.gif index ad77e46ca..39697f578 100644 Binary files a/assets/mars.gif and b/assets/mars.gif differ diff --git a/assets/moridian.gif b/assets/moridian.gif index 53a0c11da..e3cc00ab8 100644 Binary files a/assets/moridian.gif and b/assets/moridian.gif differ diff --git a/assets/move.gif b/assets/move.gif index 9fe14fe68..84ee509b2 100644 Binary files a/assets/move.gif and b/assets/move.gif differ diff --git a/assets/ppt-process.gif b/assets/ppt-process.gif deleted file mode 100644 index 8ae21335e..000000000 Binary files a/assets/ppt-process.gif and /dev/null differ diff --git a/assets/ppt-process.webp b/assets/ppt-process.webp new file mode 100644 index 000000000..4f8589f17 Binary files /dev/null and b/assets/ppt-process.webp differ diff --git a/assets/saturn+sun.gif b/assets/saturn+sun.gif index 1f54e0bd7..fbda7381b 100644 Binary files a/assets/saturn+sun.gif and b/assets/saturn+sun.gif differ diff --git a/assets/shiba.gif b/assets/shiba.gif index e76d85f57..7ba2aa99d 100644 Binary files a/assets/shiba.gif and b/assets/shiba.gif differ diff --git a/assets/showcase/academic-paper.png b/assets/showcase/academic-paper.png index 5df7486b4..b1dc372d6 100644 Binary files a/assets/showcase/academic-paper.png and b/assets/showcase/academic-paper.png differ diff --git a/assets/showcase/annual-report.png b/assets/showcase/annual-report.png index e57234dbf..189458e06 100644 Binary files a/assets/showcase/annual-report.png and b/assets/showcase/annual-report.png differ diff --git a/assets/showcase/budget-tracker.png b/assets/showcase/budget-tracker.png index f6cd5138c..105ad72a8 100644 Binary files a/assets/showcase/budget-tracker.png and b/assets/showcase/budget-tracker.png differ diff --git a/assets/showcase/employee-handbook.png b/assets/showcase/employee-handbook.png index eeaf3ba0c..fec60c376 100644 Binary files a/assets/showcase/employee-handbook.png and b/assets/showcase/employee-handbook.png differ diff --git a/assets/showcase/excel1.gif b/assets/showcase/excel1.gif index f09b24459..6e0055a0a 100644 Binary files a/assets/showcase/excel1.gif and b/assets/showcase/excel1.gif differ diff --git a/assets/showcase/excel2.gif b/assets/showcase/excel2.gif index 42ce4292d..bce17669d 100644 Binary files a/assets/showcase/excel2.gif and b/assets/showcase/excel2.gif differ diff --git a/assets/showcase/excel3.gif b/assets/showcase/excel3.gif index 4091a1cd0..500feb734 100644 Binary files a/assets/showcase/excel3.gif and b/assets/showcase/excel3.gif differ diff --git a/assets/showcase/gradebook.png b/assets/showcase/gradebook.png index 4f90d321d..9349c7bd9 100644 Binary files a/assets/showcase/gradebook.png and b/assets/showcase/gradebook.png differ diff --git a/assets/showcase/project-proposal.png b/assets/showcase/project-proposal.png index 702c485d8..4b5c6b948 100644 Binary files a/assets/showcase/project-proposal.png and b/assets/showcase/project-proposal.png differ diff --git a/assets/showcase/sales-dashboard.png b/assets/showcase/sales-dashboard.png index 90e817136..6b23c035e 100644 Binary files a/assets/showcase/sales-dashboard.png and b/assets/showcase/sales-dashboard.png differ diff --git a/assets/showcase/word1.gif b/assets/showcase/word1.gif index ea6bcd32a..55782439f 100644 Binary files a/assets/showcase/word1.gif and b/assets/showcase/word1.gif differ diff --git a/assets/showcase/word2.gif b/assets/showcase/word2.gif index b9eab24ea..f3a9d8b8f 100644 Binary files a/assets/showcase/word2.gif and b/assets/showcase/word2.gif differ diff --git a/assets/showcase/word3.gif b/assets/showcase/word3.gif index 20b1ddbd3..700078d3c 100644 Binary files a/assets/showcase/word3.gif and b/assets/showcase/word3.gif differ diff --git a/build.sh b/build.sh index 36aee6a55..68aea7653 100755 --- a/build.sh +++ b/build.sh @@ -59,16 +59,24 @@ build_config() { echo "[$CONFIG] Building $RID -> $NAME" dotnet publish "$PROJECT" -c "$CONFIG" -r "$RID" -o "$TMPDIR" --nologo -v quiet + # Atomic replace: stage as .new alongside the target, sign there, then rename. + # Overwriting the binary in place would trash the text segment of any + # running officecli process that happens to be mmap'd on this path + # (macOS does not block ETXTBSY), leaving it stuck in uninterruptible + # `UE` state on the next code page fault. if [ -f "$TMPDIR/officecli.exe" ]; then - cp "$TMPDIR/officecli.exe" "$OUTPUT/$NAME" + cp "$TMPDIR/officecli.exe" "$OUTPUT/$NAME.new" else - cp "$TMPDIR/officecli" "$OUTPUT/$NAME" + cp "$TMPDIR/officecli" "$OUTPUT/$NAME.new" fi - # Ad-hoc codesign on macOS (required by AppleSystemPolicy) + # Ad-hoc codesign on macOS (required by AppleSystemPolicy). + # Done on the staged .new copy so the live binary is never mutated in place. if [ "$(uname -s)" = "Darwin" ] && [[ "$RID" == osx-* ]]; then - codesign -s - -f "$OUTPUT/$NAME" 2>/dev/null || true + codesign -s - -f "$OUTPUT/$NAME.new" 2>/dev/null || true fi + + mv -f "$OUTPUT/$NAME.new" "$OUTPUT/$NAME" cp "$TMPDIR/officecli.pdb" "$OUTPUT/${NAME%.*}.pdb" rm -rf "$TMPDIR" diff --git a/dev-install.sh b/dev-install.sh index b3040ced1..eb6ac25cf 100755 --- a/dev-install.sh +++ b/dev-install.sh @@ -55,16 +55,23 @@ else fi mkdir -p "$INSTALL_DIR" -cp "$TMPDIR/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME" -chmod +x "$INSTALL_DIR/$BINARY_NAME" +# Atomic replace: stage as .new alongside the target, sign there, then rename. +# Overwriting the binary in place would trash the text segment of any +# running officecli process (macOS does not block ETXTBSY), leaving it +# stuck in uninterruptible `UE` state on the next code page fault. +cp "$TMPDIR/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME.new" +chmod +x "$INSTALL_DIR/$BINARY_NAME.new" rm -rf "$TMPDIR" # macOS: remove quarantine flag and ad-hoc codesign (required by AppleSystemPolicy) +# Done on the staged .new copy so the live binary is never mutated in place. if [ "$(uname -s)" = "Darwin" ]; then - xattr -d com.apple.quarantine "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || true - codesign -s - -f "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || true + xattr -d com.apple.quarantine "$INSTALL_DIR/$BINARY_NAME.new" 2>/dev/null || true + codesign -s - -f "$INSTALL_DIR/$BINARY_NAME.new" 2>/dev/null || true fi +mv -f "$INSTALL_DIR/$BINARY_NAME.new" "$INSTALL_DIR/$BINARY_NAME" + # Hint if not in PATH case ":$PATH:" in *":$INSTALL_DIR:"*) ;; @@ -72,5 +79,5 @@ case ":$PATH:" in echo "Or add the line above to your ~/.zshrc or ~/.bashrc" ;; esac -echo "OfficeCli installed successfully!" +echo "OfficeCLI installed successfully!" echo "Run 'officecli --help' to get started." diff --git a/docs/plugin-protocol.md b/docs/plugin-protocol.md new file mode 100644 index 000000000..3d56fb0c5 --- /dev/null +++ b/docs/plugin-protocol.md @@ -0,0 +1,909 @@ +# OfficeCli Plugin Protocol + +**Status**: v1 — final draft. No backward-compatibility goal; all plugins are +pre-release and re-align with this document. +**Audience**: Plugin authors and OfficeCli contributors. + +## 1. Motivation + +OfficeCli's main repo focuses on three universal Office formats (`.docx`, `.xlsx`, +`.pptx`). To extend format support without bloating the main binary or coupling +external implementations to the main repo's license, format support is delivered +through **plugins** — independent sidecar processes discovered and invoked by the +main binary. + +Concrete drivers: + +- Legacy formats (`.doc`, `.rtf`, `.odt`) where some users need migration but the + parser is heavy and the format is fading +- Regional formats (`.hwpx`, `.hwp`) maintained by communities outside the main team +- Export targets (`.pdf`, `.epub`) where the renderer library has size, license, + or platform constraints that make in-tree bundling undesirable +- Proprietary implementations that need to stay out of the Apache-licensed main + repo + +## 2. Plugin Kinds + +A plugin declares its **kind** in its manifest. Each kind has a fixed +responsibility, lifecycle, and IPC pattern. v1 defines three kinds. + +### 2.1 `dump-reader` — read a foreign format, emit officecli commands + +Used to **migrate** a foreign format into one of main's native formats +(`.docx`/`.xlsx`/`.pptx`). The output format is declared by the plugin's +manifest `target` field. + +| Aspect | Value | +|---|---| +| Lifecycle | Short-lived (one shot) | +| Source file handle | Plugin (read-only) | +| Target file handle | Main (replays plugin's batch into a sibling native file) | +| Vocabulary | **Main's `` command vocabulary** (no plugin-defined extensions) | +| IPC | None — plugin writes JSONL (one `BatchItem` per line) to stdout and exits | +| Output extension | Sibling `.` next to the source | + +Flow: + +1. User invokes a command that opens a `.doc` file +2. Main checks for a sibling `.` next to the source. If + it exists and is newer than the source, main opens it directly and skips + steps 3–5 +3. Main spawns the plugin: ` dump ` +4. Plugin parses the source and **streams** `add`/`set`/`batch` items to stdout + as JSONL (one JSON object per line, terminated by `\n`), then exits 0 +5. Main creates a blank `` skeleton, replays the batch line-by-line, + and moves it to the sibling path. Subsequent invocations reuse the sibling + +Edits target the sibling native file, not the original source. Source-side changes +invalidate the cache automatically via mtime comparison; delete the sibling to +force reconversion. + +**Streaming requirement**: dump-reader plugins MUST emit one batch item per +line, flushed individually. Top-level JSON arrays (`[{...},{...}]`) are +rejected by main with `corrupt_batch`. Streaming gives the host's idle +watchdog (§5.6) per-item activity signal and bounds main's memory usage on +large source files. + +### 2.2 `exporter` — convert native format to a foreign target + +Used to **render** native content (`.docx`/`.xlsx`/`.pptx`) into a foreign output +file (e.g. `.pdf`). Single-direction, no editing. + +| Aspect | Value | +|---|---| +| Lifecycle | Short-lived | +| Source file handle | Plugin (reads native file, read-only) | +| Target file handle | Plugin (writes foreign file) | +| Vocabulary | None — no commands exchanged | +| IPC | None — plain CLI invocation, diagnostics on stderr | + +Flow: + +1. User invokes a view mode that targets a foreign format (e.g. + `officecli view pdf --out `). The mode name maps to the + target extension. +2. Main resolves the `(from, to)` pair to a plugin +3. Main spawns the plugin with the source path and target path +4. Plugin reads the source (using its own libraries), writes the target +5. Plugin exits 0 if the target was written successfully + +**Source path is read-only.** Exporters MUST NOT write to or modify the source +file. This is a hard requirement: main passes the source path directly without +snapshotting. Plugins that need a writable working copy MUST create their own +temp copy. + +### 2.3 `format-handler` — own a foreign format end-to-end + +Used to support a **first-class non-native format** (e.g. `.hwpx`, `.hwp`). The +plugin holds the file open for the entire session and handles all document +operations. + +| Aspect | Value | +|---|---| +| Lifecycle | Long-lived (session duration) | +| Source file handle | Plugin (read-write, same file as target) | +| Target file handle | Same as source | +| Vocabulary | **Plugin-defined** (declared in manifest, snapshotted at session start) | +| IPC | stdin/stdout (long-lived); stderr for diagnostics + heartbeat | + +Flow: + +1. User invokes a command on a `.hwpx` file +2. Main resolves `.hwpx` to a `format-handler` plugin +3. Main spawns the plugin with the file path; main writes requests to the plugin's stdin and reads replies from its stdout +4. Plugin opens the file and serves JSONL frames on stdin/stdout +5. Main and plugin exchange the **open handshake** (§5.3) — plugin replies + with its runtime capabilities and vocabulary snapshot +6. Main wraps the plugin in a `FormatHandlerProxy : IDocumentHandler`; every + operation becomes an IPC message +7. On session end, main sends `close`; plugin flushes pending writes (if any) + and exits + +### 2.4 Reserved kinds + +The following kinds are reserved for future use. Plugins MUST NOT declare them +in v1: + +- `engine` — pluggable backend for an in-tree subsystem (e.g. PDF rendering, + field refresh) +- `transformer` — converts one native format to another (e.g. `.docx → .pptx`) + +A plugin MAY declare multiple kinds in a single binary (e.g. an exporter that is +also a dump-reader). See §4. + +## 3. Plugin Discovery + +When main needs a plugin for `(kind, ext)`, it searches in this fixed order. The +first match wins. + +1. **Environment variable**: `$OFFICECLI_PLUGIN__` (absolute path to + the plugin executable). Example: `$OFFICECLI_PLUGIN_DUMP_READER_DOC`. +2. **User plugins directory**: + `~/.officecli/plugins///plugin(.exe)` +3. **Bundled plugins directory** (next to the main executable): + `

/plugins///plugin(.exe)` +4. **PATH lookup**: an executable named `officecli--` or + `officecli-` (in that priority). + +Path conventions: + +- `` uses kebab-case (`dump-reader`, `format-handler`, `exporter`) +- `` is the file extension without the leading dot (`doc`, `hwpx`, `pdf`) +- On Windows, `(.exe)` is appended automatically when searching +- Symlinks are followed + +Main caches discovery results per process invocation. Adding a plugin between +invocations is picked up immediately. + +## 4. Manifest + +Every plugin MUST respond to ` --info` by printing a single JSON object +to stdout and exiting 0. The object describes the plugin to the main binary. + +### 4.1 Required fields + +| Field | Type | Description | +|---|---|---| +| `name` | string | Stable identifier, kebab-case (e.g. `officecli-doc`) | +| `version` | string | SemVer of the plugin (e.g. `1.0.0`) | +| `protocol` | integer | Protocol major version this plugin implements. v1 plugins MUST set `1`. Main rejects mismatches with exit code 5. | +| `kinds` | array | One or more declared kinds (see §2). Common case: `["dump-reader"]` | +| `extensions` | array | File extensions this plugin handles, leading dot (`[".doc"]`) | +| `idle_timeout_seconds` | object | Idle-timeout budget per verb. See §4.2. | +| `runtime` | string | Declarative runtime tag for diagnostics only: `dotnet` / `native` / `go` / `rust` / `python` / `other`. Host does not branch on this. | + +The `target` field is **required** for `dump-reader` and MUST be one of +`"docx"`, `"xlsx"`, `"pptx"`. The `vocabulary` field is **required** for +`format-handler` (§4.4). + +### 4.2 `idle_timeout_seconds` + +Idle-timeout budgets in seconds. Main's watchdog kills the plugin when no +activity (stdout byte / RPC reply / stderr heartbeat) is observed within +this many seconds. **Total wall-clock time is not bounded** — long-running +work is fine as long as the plugin keeps producing output. + +```json +"idle_timeout_seconds": { + "default": 60, + "verbs": { + "dump": 30, + "export": 120, + "save": 30 + } +} +``` + +Rules: + +- `default` is mandatory (positive integer) +- `verbs` is optional; entries override `default` for that verb +- `0` is **not allowed in the manifest** (avoids silent never-kill). Users + can opt out at runtime via the `OFFICECLI_PLUGIN_IDLE_TIMEOUT_SECONDS` + environment variable (see below) +- Recommended defaults (informative, not normative): + - `dump-reader.dump` — 30s (streaming emit keeps idle low) + - `exporter.export` — 60s (long jobs should heartbeat; see §5.6) + - `format-handler` per-verb — 30s for reads, 60s for mutations/save + +**User override**: set `OFFICECLI_PLUGIN_IDLE_TIMEOUT_SECONDS=` in the +host environment to override the manifest budget for every verb in that +invocation (`0` disables the watchdog entirely). The override is for the +human user debugging a hung plugin — plugins themselves do not see this +variable, and it does not propagate into the plugin subprocess. + +### 4.3 Optional fields + +| Field | Type | Description | +|---|---|---| +| `description` | string | Short human-readable description | +| `target` | string | Native format the plugin produces (`"docx"`/`"xlsx"`/`"pptx"`). Required for `dump-reader`. | +| `tier` | string | Free-form tier identifier (`basic`/`pro`/`enterprise`) | +| `supports` | array | Capability tags (e.g. `["tables","images","fields"]`) | +| `limits` | object | Plugin-imposed limits (e.g. `{"maxFileSizeMb": 200}`) | +| `homepage` | string | URL | +| `license` | string | SPDX identifier | + +### 4.4 Vocabulary (format-handler only) + +Format-handler plugins MUST declare the vocabulary their proxied document model +exposes: + +```json +"vocabulary": { + "addable_types": ["page", "annotation", "formfield", "outline-item"], + "settable_props": { + "annotation": ["type", "rect", "color", "contents", "author", "opacity"], + "page": ["rotation", "mediaBox"], + "formfield": ["value", "readOnly"] + }, + "path_segments": ["/page[N]", "/page[N]/annotation[M]", "/formfield[]"] +} +``` + +Manifest vocabulary is used for **discovery and help output**. At session +start, the plugin returns a runtime **vocabulary snapshot** in the open +handshake reply (§5.3), which may differ from the manifest (e.g. extra +aliases). The host trusts the snapshot for validation. + +**Vocabulary is documentation, not a runtime gate**: main does not reject +commands that fall outside the declared vocabulary. Plugins self-report +unsupported keys via the `set` reply's `unsupported_properties` list. This +follows the project-wide "handler-as-truth" principle. + +### 4.5 Example manifests + +`officecli-doc` (dump-reader): +```json +{ + "name": "officecli-doc", + "version": "1.0.0", + "protocol": 1, + "kinds": ["dump-reader"], + "extensions": [".doc"], + "target": "docx", + "runtime": "dotnet", + "idle_timeout_seconds": { "default": 60, "verbs": { "dump": 30 } }, + "tier": "basic", + "supports": ["paragraphs", "runs", "tables", "images", "lists"] +} +``` + +`officecli-pdf` (exporter): +```json +{ + "name": "officecli-pdf", + "version": "0.1.0", + "protocol": 1, + "kinds": ["exporter"], + "extensions": [".pdf"], + "runtime": "dotnet", + "idle_timeout_seconds": { "default": 60, "verbs": { "export": 120 } }, + "supports": ["from:docx", "from:xlsx", "from:pptx"] +} +``` + +`officecli-hwpx` (format-handler): +```json +{ + "name": "officecli-hwpx", + "version": "0.9.0", + "protocol": 1, + "kinds": ["format-handler"], + "extensions": [".hwpx"], + "runtime": "dotnet", + "idle_timeout_seconds": { "default": 30, "verbs": { "save": 60 } }, + "vocabulary": { + "addable_types": ["paragraph", "run", "table", "image", "footnote"], + "settable_props": { }, + "path_segments": [ ] + } +} +``` + +## 5. Invocation + +Beyond `--info`, each kind has its own subcommand surface. + +### 5.1 dump-reader + +``` + dump [--media-dir ] +``` + +- ``: absolute path to the file to read +- `--media-dir`: optional scratch directory the plugin may use for transient + files (e.g. extracted images referenced by command paths) + +Main sets the `OFFICECLI_BIN` environment variable to the path of the running +officecli binary, so plugins that produce an intermediate `.docx` (e.g. via an +external converter) can shell out to `officecli dump ` and pipe +its output to stdout. Plugins that don't need this can ignore the variable. + +**Output format**: JSONL — one JSON object per line, terminated by `\n`, +each line `flush`ed individually. Schema per line matches one entry of +`officecli batch --commands`: + +```jsonl +{"command":"add","parent":"/body","type":"paragraph","props":{"text":"Hello"}} +{"command":"set","path":"/body/paragraph[1]","props":{"bold":"true"}} +``` + +A top-level JSON array on a single line is **rejected** with `corrupt_batch`. + +Diagnostics go to stderr or `--log-file`. The plugin exits 0 on success; non-zero +codes follow §6.5. + +### 5.2 exporter + +``` + export --out [--options ] +``` + +- ``: native format file (`.docx`/`.xlsx`/`.pptx`) — **read-only** +- `--out`: target path for the exported file +- `--options`: optional backend-specific options as a JSON string + +The plugin MUST NOT write to or modify ``. Main relies on this +to skip defensive snapshotting. + +### 5.3 format-handler + +``` + open +``` + +The plugin reads request frames from **stdin** and writes reply frames to +**stdout** (one JSON object per line, terminated by `\n`). Diagnostic +output and heartbeat lines (§5.6) go on **stderr**. Anything the plugin +writes to stdout that is not a valid envelope is a plugin bug: main reports +it as `protocol_mismatch` and the session enters the broken state. + +**Open handshake** (mandatory first exchange before any user command): + +Main sends: +```json +{"protocol":1,"msg_type":"open","path":"","editable":true} +``` + +Plugin replies: +```json +{"protocol":1,"msg_type":"ok","result":{ + "capabilities":{ + "commands":["add","set","get","query","remove","move","save","raw","raw-set"], + "features":["save","extract-binary"] + }, + "vocabulary":{ + "addable_types":[...], + "settable_props":{...}, + "path_segments":[...] + } +}} +``` + +Failure to handshake within the verb's idle timeout terminates the session. +The host caches the returned capabilities and vocabulary; subsequent +commands not present in `commands` are short-circuited with +`unsupported_command` without round-tripping. + +After handshake, each request gets exactly one reply before the next request +is sent (§6.2). + +#### Proxied verbs + +Request envelope (main → plugin): +```json +{"protocol":1,"msg_type":"command","command":"","args":{...},"props":{...}} +``` + +**Read path:** + +| `command` | `args` keys | `result` shape on `ok` | +|---|---|---| +| `view` | `mode` (`text`/`annotated`/`outline`/`stats`/`issues`), `start`/`end`/`max_lines`/`cols`/`type`/`limit`/`format` | string (or JSON object when `format=json`); for `mode=issues`, an array of issue objects | +| `get` | `path`, `depth` | DocumentNode JSON object | +| `query` | `selector` | array of DocumentNode | +| `validate` | (none) | array of `{error_type,description,path,part}` | + +**Mutation path** (envelope carries `args` and `props` separately; `props` is +the user's `--prop key=value` dictionary, always string-to-string): + +| `command` | `args` keys | `props` | `result` shape on `ok` | +|---|---|---|---| +| `set` | `path` | yes | object `{"unsupported_properties":["key1",...]}` (empty array = all applied) | +| `add` | `parent_path`, `type`, optional `position` | yes | object `{"path":"...","unsupported_properties":[...]}` | +| `remove` | `path` | no | string or null — optional warning text (e.g. cells shifted) | +| `move` | `source_path`, optional `target_parent_path`, optional `position` | no | string — new path | +| `copy` | `source_path`, `target_parent_path`, optional `position` | no | string — new path | +| `raw` | `part_path`, optional `start_row`/`end_row`/`cols` | no | string — raw XML (or CSV-of-rows for spreadsheet parts) | +| `raw_set` | `part_path`, `xpath`, `action`, optional `xml` | no | null | +| `add_part` | `parent_part_path`, `part_type` | optional | object `{"rel_id":"...","part_path":"..."}` | +| `extract_binary` | `path`, `dest_path` | no | object `{"found":true,"content_type":"...","byte_count":N}` or `{"found":false}` | + +`position` (when present) is `{"index":N}` OR `{"after":""}` OR +`{"before":""}` — at most one field set; all-null means append. + +**Numeric tolerance**: `byte_count` and similar integer fields MUST be JSON +numbers with no fractional part. Hosts SHOULD accept either int or +double-encoded integer forms (`42` and `42.0`) to absorb runtime drift across +languages. + +#### `save` + +```json +{"protocol":1,"msg_type":"save"} +``` + +`save` is **normative for format-handler plugins that accept mutations**. +The plugin MUST flush all pending writes to disk before replying `ok`. A +no-op acknowledgement is non-conformant and breaks main's crash-recovery +expectations. `plugins lint` verifies that a mutation followed by `save` is +durable by reopening the file from disk after the reply. + +#### `close` + +```json +{"protocol":1,"msg_type":"close"} +``` + +Plugin acknowledges with `ok`, flushes (implicit `save` if mutations were +applied without an explicit `save`), and exits 0. + +### 5.4 Universal options + +Each plugin subcommand SHOULD accept: + +- `--log-file `: append diagnostic output here instead of stderr +- `--quiet`: suppress non-error output + +These are plugin-side conventions. The host's own idle-watchdog override is +the `OFFICECLI_PLUGIN_IDLE_TIMEOUT_SECONDS` env var (§4.2) — host does not +forward CLI flags into the plugin process for timeout purposes. + +### 5.5 Cross-runtime conventions + +To keep .NET / Go / Rust / native plugins interchangeable, all plugins MUST: + +- Emit UTF-8 **without** BOM on stdout and stderr +- Use `\n` (not `\r\n`) as line separator on all platforms, including Windows +- Use **snake_case** for all JSON keys (manifest, IPC envelopes, error bodies) +- Return one of the documented exit codes (§6.5); non-zero codes that are + not documented are reported as `internal_error` + +### 5.6 Idle-timeout watchdog & heartbeat + +Main runs a watchdog thread for every spawned plugin process: + +- Any byte written to stdout (dump-reader, format-handler reply) **resets** the + idle timer +- A line on stderr matching `{"heartbeat":true}` (optionally with extra + fields) **resets** the idle timer without producing diagnostic noise. The + heartbeat line is consumed by the watchdog and not surfaced to the user +- When `now - last_activity > idle_timeout`, main `Kill(entire_process_tree)` + and reports `plugin_idle_timeout` (exit code 6) +- `--timeout 0` disables the watchdog; manifest cannot disable + +Long opaque operations (exporter rendering, format-handler `save` on large +files) SHOULD emit periodic heartbeats. Plugins that stream output +naturally (dump-reader JSONL) do not need heartbeats. + +## 6. IPC Protocol + +Only `format-handler` exchanges live messages with main; the framing below +applies to that kind. (`dump-reader` and `exporter` are short-lived and use the +simpler stdout / exit-code contracts described in §5.1 and §5.2.) + +### 6.1 Transport + +Three standard streams, no auxiliary IPC channel: + +- **stdin** — main writes request envelopes here, plugin reads them +- **stdout** — plugin writes reply envelopes here, main reads them +- **stderr** — plugin writes diagnostics and heartbeat lines here (§5.6) + +The choice is deliberate: stdin/stdout is the same shape `dump-reader` and +`exporter` already use, every language has it built-in (no `NamedPipeClient` +or `UnixStream` wrapper to learn), and it sidesteps macOS's 104-byte +socket-path limit. The trade-off is one rule plugins MUST follow: stdout +carries protocol frames only — debug output goes to stderr or +`--log-file`. Main does not defend against polluted stdout; non-envelope +content is reported as `protocol_mismatch` and the session enters broken. + +### 6.2 Framing & concurrency + +UTF-8 text without BOM. One JSON object per line, terminated by `\n`. The +protocol is **request/response**: every client message receives exactly one +server reply before the next message is sent. For `format-handler`, **main +is the client** and **plugin is the server**. + +Main MUST serialize requests per session. Callers in main that share a +single `FormatHandlerSession` MUST go through the session's internal mutex; +plugins MAY assume one request is in flight at a time. + +### 6.3 Message envelope + +Every message MUST include: + +```json +{ + "protocol": 1, + "msg_type": "", + ... type-specific fields ... +} +``` + +### 6.4 Message types + +#### Request types (client → server) + +| `msg_type` | Body | +|---|---| +| `open` | `{ "path": "", "editable": }` (handshake, §5.3) | +| `command` | `{ "command": "add"\|"set"\|..., "args": {...}, "props": {...} }` | +| `save` | `{}` (normative flush, §5.3) | +| `close` | `{}` | +| `ping` | `{}` (liveness check; resets idle timer) | + +#### Response types (server → client) + +| `msg_type` | Body | +|---|---| +| `ok` | `{ "result": }` | +| `error` | `{ "error": { "code": "", "message": "...", "detail": "..." } }` | + +#### Server-pushed events (format-handler only) + +| `msg_type` | Body | +|---|---| +| `event` | `{ "kind": "warning"\|"info", "message": "..." }` | + +Events are unsolicited and do not consume a reply slot; main MAY ignore them. + +### 6.5 Exit codes + +When a plugin process terminates: + +| Code | Meaning | +|---|---| +| `0` | Success | +| `2` | Corrupt input file | +| `3` | Feature unsupported in this build | +| `4` | License expired | +| `5` | Protocol mismatch | +| `6` | Idle timeout (host-imposed; plugins do not emit this themselves) | +| `64`-`78` | Reserved (sysexits.h) | +| other | Plugin bug; main reports as `internal_error` | + +### 6.6 Error codes (in `error.code`) + +Plugins SHOULD use these codes when applicable: + +| Code | Meaning | +|---|---| +| `invalid_request` | Malformed message | +| `unsupported_command` | Recognized message but unimplemented | +| `unsupported_feature` | Recognized command but feature not in this build | +| `invalid_argument` | Argument failed validation | +| `not_found` | Target path/element does not exist | +| `corrupt_input` | Source file is malformed or unreadable | +| `corrupt_batch` | dump-reader output is not valid JSONL | +| `license_expired` | Commercial plugin's license check failed | +| `protocol_mismatch` | Manifest protocol version differs from main's | +| `plugin_idle_timeout` | Host watchdog fired | +| `plugin_stream_closed` | stdin/stdout reached EOF before handshake or mid-session | +| `internal_error` | Catch-all for plugin bugs | + +Codes are extensible; main treats unknown codes as `internal_error`. + +### 6.7 Session lifecycle state machine + +``` + spawn process + (none) ─────────────────────────────────► spawning + │ + open handshake │ idle timer running + succeeded on │ + stdin/stdout ▼ + ready + │ + command request sent │ command reply received + ────────────► │ ◄──────────── + ▼ + busy + │ + │ stdin write failure + │ OR stdout EOF / read failure + │ OR idle timeout + │ OR malformed reply + ▼ + broken + │ + │ Dispose / Kill + ▼ + closed +``` + +Rules: + +- Any IO failure or watchdog kill transitions to **broken**. Once broken, + subsequent `Send` calls fail fast with `plugin_stream_closed`; the + session is not auto-respawned (callers Dispose and re-open if needed) +- `close` reply transitions cleanly to **closed** +- The process is `Kill(entire_process_tree)`'d on transition to **closed** + if it has not exited within 2 seconds of `close` reply (or immediately on + transition from **broken**) + +## 7. Vocabulary Contract + +### 7.1 Universal protocol shell (all kinds) + +These elements are stable across all plugins and all kinds: + +- Message envelope shape (§6.3) +- Command verbs: `add`, `set`, `remove`, `move`, `get`, `query`, `batch`, + `raw_set` +- Path syntax: `/segment[N]` with `[N]` 1-based index OR `[]` named + reference +- Error and exit code namespaces (extensible) + +### 7.2 Per-format vocabulary + +The specific **types** (`paragraph`/`page`/`cell`/...), **property names** +(`bold`/`fontsize`/`rect`/...), and **value formats** (`12pt`/`#FF0000`/...) are +not universal. They depend on which document model is at the other end: + +- For `dump-reader`, the receiving model is main's `WordprocessingDocument` (or + the spreadsheet/presentation equivalent for non-docx targets), so the + vocabulary is main's `` vocabulary (published as + `schemas/word-vocabulary.json` etc.) +- For `format-handler`, the model is the plugin's own; the plugin declares its + vocabulary in the manifest and reaffirms it via the open handshake +- For `exporter`, there is no command vocabulary + +## 8. Installation + +The protocol does **not** mandate any installation mechanism. As long as the +plugin executable ends up at one of the discovery paths (§3), it works. + +Common installation channels: + +- **Manual**: download a release archive, extract to `~/.officecli/plugins/...` +- **Bundled distribution**: main's release archive includes a `plugins/` + directory next to the executable +- **Built-in installer** (recommended for users): `officecli plugins install ` +- **Package managers**: `dotnet tool install`, `winget`, `brew`, `apt`, `scoop` +- **Enterprise deployment**: place binaries via IT distribution + +The built-in installer consults a registry (default: +`https://officecli.ai/plugins/registry.json`; configurable for private mirrors) +which lists approved plugins, versions, download URLs, and SHA-256 hashes. + +## 9. Writing a Plugin + +### 9.1 Minimum dump-reader (C#) + +```csharp +using System.Text.Json; + +if (args[0] == "--info") { + Console.WriteLine(JsonSerializer.Serialize(new { + name = "officecli-doc-minimal", + version = "0.0.1", + protocol = 1, + kinds = new[] { "dump-reader" }, + extensions = new[] { ".doc" }, + target = "docx", + runtime = "dotnet", + idle_timeout_seconds = new { @default = 30 } + })); + return 0; +} + +// args: dump +string sourcePath = args[1]; + +// Parse source file (your library here) and emit one JSON object per line. +// Flush each line individually so main's idle watchdog sees activity. +var stdout = Console.Out; +stdout.WriteLine(JsonSerializer.Serialize(new { + command = "add", + parent = "/body", + type = "paragraph", + props = new { text = "Hello from .doc" } +})); +stdout.Flush(); +// ... more items ... +return 0; +``` + +### 9.2 Minimum exporter (Go) + +```go +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" +) + +func main() { + if len(os.Args) > 1 && os.Args[1] == "--info" { + json.NewEncoder(os.Stdout).Encode(map[string]any{ + "name": "officecli-pdf-min", + "version": "0.0.1", + "protocol": 1, + "kinds": []string{"exporter"}, + "extensions": []string{".pdf"}, + "runtime": "go", + "idle_timeout_seconds": map[string]any{ + "default": 60, + "verbs": map[string]int{"export": 120}, + }, + }) + return + } + + // args: export --out + // MUST NOT write to source-file. + source := os.Args[2] + var target string + for i, a := range os.Args { + if a == "--out" && i+1 < len(os.Args) { + target = os.Args[i+1] + } + } + + // Heartbeat on stderr for long jobs: + go func() { + for { + fmt.Fprintln(os.Stderr, `{"heartbeat":true}`) + time.Sleep(20 * time.Second) + } + }() + + cmd := exec.Command("soffice", "--headless", "--convert-to", "pdf", + "--outdir", "/tmp/officecli-pdf", source) + if err := cmd.Run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(3) + } + // ... move output to target ... +} +``` + +### 9.3 Minimum format-handler (C#, sketch) + +```csharp +// args: open +// stdin = requests from main, stdout = replies to main, +// stderr = diagnostics + heartbeat. +var stdin = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false)); +var stdout = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)) +{ + NewLine = "\n", + AutoFlush = true, +}; + +while (true) { + var line = stdin.ReadLine(); + if (line == null) break; + var msg = JsonNode.Parse(line)!; + switch ((string)msg["msg_type"]!) { + case "open": + // load file, return capabilities + vocabulary snapshot + stdout.WriteLine(JsonSerializer.Serialize(new { + protocol = 1, + msg_type = "ok", + result = new { + capabilities = new { + commands = new[] { "get", "set", "save" }, + features = Array.Empty() + }, + vocabulary = /* ... */ new {} + } + })); + break; + case "save": + // MUST actually flush to disk before replying ok + File.WriteAllBytes(filePath, currentBytes); + stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}"""); + break; + case "close": + stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}"""); + return 0; + // ... command dispatch ... + } +} +``` + +## 10. Stability Commitments + +### 10.1 Main → Plugins + +Once protocol v1 is ratified, main commits to: + +1. **Protocol shell** is stable for v1. Adding new optional message types is + allowed; removing or changing types requires a v2 bump. +2. **Native vocabulary** (relevant to `dump-reader`): additions allowed; + deletions or renames require a deprecation cycle of at least two minor + releases with the old name accepted as an alias. +3. **Path syntax** does not change. +4. **Error/exit code semantics** do not change. Adding new codes is allowed. +5. **Schema files** (`schemas/word-vocabulary.json`, etc.) are released + alongside main and follow the same versioning. + +### 10.2 Plugins → Main + +Plugin authors should: + +1. Treat `--info` output schema as stable per protocol major version. +2. Implement graceful degradation when main lacks expected capabilities. +3. Provide a meaningful exit code on failure (don't silently exit 1 for every + error). +4. Avoid writing to paths other than `--media-dir`, the declared output file, + or temp files the plugin owns. + +## 11. FAQ + +**Q: Can plugins be in any language?** +A: Yes. The protocol is JSONL over stdin/stdout. Any language with +subprocess and standard-stream support works. .NET plugins can optionally use the +`OfficeCli.Contracts` NuGet package for type-safe types. + +**Q: How does main know which plugin to use when several are installed?** +A: Discovery order (§3) is fixed and first-match-wins. For multiple installed +plugins for the same extension, users select via env var or explicit +`--plugin` flag. + +**Q: Can a plugin be closed-source / commercial?** +A: Yes. Plugins are independent binaries with their own license. License +check failures exit 4 (`license_expired`). + +**Q: What if the plugin crashes?** +A: Main detects non-zero exit and surfaces a clear error. Partial state in +main's in-memory document is discarded; no corrupt files are written. + +**Q: What if the plugin hangs?** +A: Main's idle watchdog (§5.6) kills it when no output is observed within +the manifest-declared `idle_timeout_seconds`. Long jobs heartbeat on stderr +to stay alive. + +**Q: Why no total wall-clock timeout?** +A: Large .doc files legitimately take minutes to dump; Word-interop PDF +export of large workbooks can take hours. A wall-clock cap punishes correct +behavior. Idle timeout catches actual hangs without false positives. + +**Q: How does this differ from MCP?** +A: MCP exposes officecli to AI clients; plugins extend officecli's format +support. The two are complementary. + +## 12. Versioning + +This document tracks **protocol** version, distinct from main repo version. + +- v1.x: Additive changes only (new optional fields, new message types, new + error codes). Backward-compatible. +- v2.x: Breaking changes (removed/renamed fields, changed semantics). + +Main repo declares supported protocol version(s) via `officecli --version`. +Plugins declare their target protocol in manifest. Main rejects plugins +whose major protocol version differs from main's supported version, exiting +the plugin process with code 5 and surfacing `protocol_mismatch` to the user. + +## 13. Open Questions (post-v1) + +- Should `format-handler` plugins support concurrent multi-document sessions in + one process? (v1: no, one process per open document) +- Should the registry support package signing? (Likely yes for v1.1) +- Should `capabilities` queries return JSON Schema fragments inline, or only + list names? (Currently: names; consider inline schema in v1.1) +- Host-driven session pooling for format-handler (kill idle sessions to free + memory). Not in v1; revisit if process count becomes a real problem. + +--- + +*This document is the source of truth for the OfficeCli Plugin Protocol v1. +Pre-release plugins re-align with this document; post-ratification changes +follow §10 and §12.* diff --git a/examples/README.md b/examples/README.md index 03e0145f1..b1b6903a8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,34 +7,50 @@ Comprehensive examples demonstrating OfficeCLI capabilities for Word, Excel, and ``` examples/ ├── README.md # This file -├── word/ # 📄 Word examples (3 scripts) -│ ├── README.md -│ ├── gen-formulas.sh -│ ├── gen-complex-tables.sh -│ ├── gen-complex-textbox.sh -│ └── outputs/ -├── excel/ # 📊 Excel examples (2 scripts) -│ ├── README.md -│ ├── gen-beautiful-charts.sh -│ ├── gen-charts-demo.sh -│ └── outputs/ -└── ppt/ # 🎨 PowerPoint (3 scripts + 14 style templates) - ├── README.md - ├── gen-beautiful-pptx.sh - ├── gen-animations-pptx.sh - ├── gen-video-pptx.py - ├── outputs/ - └── templates/ # 14 Professional Style Templates ⭐ - ├── README.md - └── styles/ # (all with pre-generated PPTs) - ├── dark--*/ (14 dark styles) - ├── light--*/ (8 light styles) - ├── warm--*/ (5 warm styles) - ├── vivid--*/ (2 vivid styles) - ├── bw--*/ (3 black & white) - └── mixed--*/ (1 mixed style) +├── word/ # 📄 Word examples +│ ├── formulas.sh / formulas.docx +│ ├── tables.sh / tables.docx +│ ├── textbox.sh +│ ├── numbering-showcase.sh / numbering-showcase.docx +│ └── revisions.{sh,md,docx} # tracked-change (revision) API +├── excel/ # 📊 Excel examples +│ ├── charts.sh / charts.xlsx # Master chart showcase +│ ├── charts/ # Per-type chart scripts +│ │ ├── charts-demo.{sh,md,xlsx} +│ │ └── charts-.{md,py,xlsx} +│ │ (basic, advanced, extended, area, bar, boxwhisker, +│ │ bubble, column, combo, histogram, line, pie, radar, +│ │ scatter, stock, waterfall) +│ └── pivot-tables.py / pivot-tables.xlsx +└── ppt/ # 🎨 PowerPoint examples + ├── presentation.{md,sh,pptx} + ├── animations.{md,sh,pptx} + ├── video.{md,py,pptx} + ├── 3d-model.{md,sh,pptx} + ├── charts/ # PowerPoint chart showcases + │ └── charts-.{md,py,pptx} + │ (column, bar, line, pie, doughnut, area, scatter, + │ bubble, radar, stock, combo, waterfall, 3d, advanced) + ├── tables/ # PowerPoint table showcases + │ └── tables-.{md,sh,pptx} + │ (basic, styled, merged, borders, rows-cols, financial) + ├── transitions/ # Slide transition showcases + │ └── transitions-.{md,sh,pptx} + │ (basic, directional, shapes, bands, dynamic, modern, random, timing, morph) + ├── shapes/ # Primitive shape building blocks + │ ├── shapes-basic.{md,sh,pptx} # geometries, fills, outlines, rotation, basic effects + │ ├── shapes-connectors.{md,sh,pptx} # straight/elbow/curve connectors + groups + │ ├── shapes-effects.{md,sh,pptx} # autoFit, flip, image fill, 3D, softEdge, links, zorder + │ └── shapes-typography.{md,sh,pptx} # paragraph/char spacing, kern, case, RTL, font.cs, lang + ├── textboxes/ # Text container primitives + │ ├── textboxes-basic.{md,sh,pptx} # alignment, bullets, runs, per-script fonts + │ └── textboxes-advanced.{md,sh,pptx} # per-paragraph overrides, indents, per-run typography + └── pictures/ # Image embedding + └── pictures-basic.{md,py,pptx} # src forms, crop, rotation, clickable links ``` +Each example follows the same trio: `.md` (walkthrough), `.sh`/`.py` (build script), `.` (pre-generated output). + --- ## 🚀 Quick Start @@ -44,121 +60,79 @@ examples/ **Word (.docx):** ```bash cd word -bash gen-formulas.sh # LaTeX math formulas -bash gen-complex-tables.sh # Styled tables -bash gen-complex-textbox.sh # Formatted text boxes +bash formulas.sh # LaTeX math formulas +bash tables.sh # Styled tables +bash textbox.sh # Formatted text boxes +bash numbering-showcase.sh # List/numbering styles +bash revisions.sh # Tracked-change (revision) API — ins/del/format/move/cellChange ``` **Excel (.xlsx):** ```bash cd excel -bash gen-beautiful-charts.sh # Professional charts -bash gen-charts-demo.sh # 14+ chart types +bash charts.sh # Master chart showcase +bash charts/charts-demo.sh # 14+ chart types +python charts/charts-line.py # Single-type example (any charts/charts-.py) +python pivot-tables.py # Pivot tables ``` **PowerPoint (.pptx):** ```bash cd ppt -bash gen-beautiful-pptx.sh # Morph transitions -bash gen-animations-pptx.sh # Animation effects -python gen-video-pptx.py # Video embedding -``` - -### Professional Style Templates - -```bash -cd ppt/templates/styles/dark--investor-pitch -# View pre-generated PPT -open template.pptx - -# Or regenerate -bash build.sh +bash presentation.sh # Morph transitions / full deck +bash animations.sh # Animation effects +python video.py # Video embedding +bash 3d-model.sh # 3D model embedding +python charts/charts-column.py # PowerPoint chart examples (any charts/charts-.py) +bash tables/tables-basic.sh # Tables — minimal create + populate +bash tables/tables-styled.sh # 9 built-in styles + banding flags + rowHeight/name= +bash tables/tables-merged.sh # gridSpan horizontal merge +bash tables/tables-borders.sh # Per-side / per-cell borders +bash tables/tables-rows-cols.sh # add row/column, per-row height, gridSpan + merge.down +bash tables/tables-financial.sh # End-to-end financial deck +bash transitions/transitions-basic.sh # cut/fade/dissolve/flash + 'none' clear +bash transitions/transitions-directional.sh # push/wipe/cover/uncover × direction matrix +bash transitions/transitions-shapes.sh # circle/diamond/wedge/wheel/zoom +bash transitions/transitions-bands.sh # blinds/strips/split/checker +bash transitions/transitions-dynamic.sh # 2010+ Exciting gallery (vortex/flip/...) +bash transitions/transitions-modern.sh # 2013+ Exciting gallery (pageCurl/airplane/origami/...) +bash transitions/transitions-random.sh # newsflash / random +bash transitions/transitions-timing.sh # speed, duration, advanceTime, advanceClick +bash transitions/transitions-morph.sh # 2016+ Morph tweening +bash shapes/shapes-basic.sh # geometries, fills, outlines, rotation, basic effects +bash shapes/shapes-connectors.sh # straight/elbow/curve connectors + groups +bash shapes/shapes-effects.sh # autoFit, flip, image fill, 3D, softEdge, links, zorder +bash shapes/shapes-typography.sh # spacing, kern, case, RTL direction, font.cs, lang +bash textboxes/textboxes-basic.sh # alignment, bullets, runs, per-script fonts +bash textboxes/textboxes-advanced.sh # per-paragraph overrides, indents, per-run typography +python pictures/pictures-basic.py # picture src/crop/rotation/links (needs Pillow) ``` -👉 **[Browse all 35 styles →](ppt/templates/)** (all with pre-generated PPTs) - --- ## 📚 Documentation by Type ### 📄 [Word Examples →](word/) -**3 scripts demonstrating:** - Mathematical formulas (LaTeX) -- Complex table creation -- Text styling and formatting - -**Key Techniques:** -- Paragraph and run manipulation -- Table structure and styling -- Font and color formatting -- Document structure navigation - ---- +- Complex tables +- Text boxes and styling +- Numbering / list showcases ### 📊 [Excel Examples →](excel/) -**2 scripts demonstrating:** -- Professional chart creation -- Multiple chart types (14+) -- Data visualization - -**Key Techniques:** -- Cell value and formula manipulation -- Chart creation and styling -- Data range selection -- Number formatting - ---- +- Master and per-type chart scripts (line, bar, pie, scatter, stock, waterfall, …) +- Pivot tables +- Number formatting and styling ### 🎨 [PowerPoint Examples →](ppt/) -**3 scripts + 35 professional style templates:** -- Morph transitions -- Animation effects -- Video embedding -- 35 design styles (15 ready-to-use) - -**Key Techniques:** -- Slide creation and layout -- Shape positioning and styling -- Transitions and animations -- Media embedding -- Professional design patterns - -**Style Categories:** -- 🌑 **Dark** (14) - Tech, corporate, futuristic -- ☀️ **Light** (8) - Clean, professional, product showcases -- 🧡 **Warm** (5) - Friendly, lifestyle, organic brands -- 🌈 **Vivid** (2) - Energetic, youthful marketing -- ⬛ **Black & White** (3) - Minimalist, sophisticated -- 🎨 **Mixed** (1) - Bold architectural designs - ---- - -## 🎓 Learning Path - -### Beginner (Start Here) -1. **Word** - [`gen-formulas.sh`](word/gen-formulas.sh) -2. **Excel** - [`gen-charts-demo.sh`](excel/gen-charts-demo.sh) -3. **PowerPoint** - Simple shape creation - -**Learn:** Basic commands, file structure, properties - ---- - -### Intermediate -4. **Word** - [`gen-complex-tables.sh`](word/gen-complex-tables.sh) -5. **Excel** - [`gen-beautiful-charts.sh`](excel/gen-beautiful-charts.sh) -6. **PowerPoint** - [`gen-animations-pptx.sh`](ppt/gen-animations-pptx.sh) - -**Learn:** Batch operations, styling, advanced properties - ---- - -### Advanced -7. **Style Templates** - Explore [professional styles](ppt/templates/) -8. **PowerPoint** - [`gen-beautiful-pptx.sh`](ppt/gen-beautiful-pptx.sh) -9. **Python Integration** - [`gen-video-pptx.py`](ppt/gen-video-pptx.py) - -**Learn:** Morph transitions, complex layouts, design patterns, automation +- Slide / shape construction +- Morph transitions and animations +- Video and 3D model embedding +- Native chart examples (column, bar, line, pie, doughnut, area, scatter, bubble, radar, stock, combo, waterfall, 3D, advanced) +- Tables — basic, built-in styles, merged cells, borders, row/column ops, real-world financial deck +- Slide transitions — all 59 schema tokens covered across 9 trios: basic, directional, shape, band, dynamic 3D (p14), modern (p15 — Page Curl, Airplane, Origami, …), random, timing, and Morph +- Shapes — full pptx/shape property surface across 4 trios: geometries + fills + outlines + rotation + basic effects (basic), straight/elbow/curve connectors + groups (connectors), autoFit + flip + image-fill + 3D scene + softEdge + click links + zorder (effects), paragraph/char spacing + kerning + smallCaps + RTL + complex-script font + BCP-47 lang (typography) +- Textboxes — alignment, bulleted/numbered lists, run-by-run rich text (bold/italic/color/super/sub/strike), per-script fonts (Latin/EastAsian), vertical alignment and padding +- Pictures — file path / URL / data-URI / `name=` for `src=`, all crop forms (symmetric, V,H, per-edge L/T/R/B), rotation, clickable links (URL / slide jump / named action) --- @@ -242,6 +216,9 @@ officecli set report.docx /body/p[1] --prop color=FF0000 | `outline` | Structure | `officecli view file.docx outline` | | `stats` | Statistics | `officecli view file.docx stats` | | `issues` | Problems | `officecli view file.docx issues` | +| `html` | HTML preview | `officecli view file.docx html` | +| `svg` | SVG preview | `officecli view file.docx svg` | +| `forms` | Form fields | `officecli view file.docx forms` | --- @@ -258,11 +235,11 @@ officecli set report.docx /body/p[1] --prop color=FF0000 officecli query data.xlsx "cell[formula~=SUM]" --json | jq ``` -3. **Check help for properties:** +3. **Check help for properties** (schema reference is under the `help` verb): ```bash - officecli docx set paragraph - officecli xlsx set cell - officecli pptx set shape + officecli help docx set paragraph + officecli help xlsx set cell + officecli help pptx set shape ``` 4. **Validate after changes:** @@ -270,25 +247,17 @@ officecli set report.docx /body/p[1] --prop color=FF0000 officecli validate document.docx ``` -5. **Use resident mode for performance:** +5. **Use resident mode for performance** (3+ operations on same file): ```bash - # For 3+ operations on same file officecli open file.pptx # ... multiple commands ... officecli close file.pptx ``` -6. **Batch for complex operations:** - - Single open/save cycle - - Atomic transactions - - Better performance - --- ## 🤝 Contributing Examples -Want to add an example? Follow this structure: - 1. **Create script** with clear comments 2. **Test and verify** output 3. **Add to appropriate directory** (word/excel/ppt) @@ -307,7 +276,7 @@ FILE="output.docx" officecli create "$FILE" # ... your commands ... officecli validate "$FILE" -echo "✅ Created: $FILE" +echo "Created: $FILE" ``` --- @@ -316,26 +285,24 @@ echo "✅ Created: $FILE" - **[SKILL.md](../SKILL.md)** - Complete command reference for AI agents - **[README.md](../README.md)** - Project overview and installation -- **[API Documentation](../docs/)** - Detailed API reference --- ## 🆘 Getting Help -**Command help:** +**Top-level help:** ```bash -officecli --help -officecli docx --help -officecli docx set --help -officecli pptx set shape +officecli --help # CLI usage +officecli help # Schema reference entry point +officecli help docx # All docx elements +officecli help docx set # Elements that support `set` for docx +officecli help docx set paragraph # Settable properties on paragraph +officecli help docx paragraph --json # Raw schema JSON +officecli help all # Flat dump of every (format, element, property) ``` -**Three-layer help navigation:** -```bash -officecli pptx set # All settable elements -officecli pptx set shape # Shape properties -officecli pptx set shape.fill # Fill property details -``` +Format aliases: `word→docx`, `excel→xlsx`, `ppt`/`powerpoint→pptx`. +Verbs: `add`, `set`, `get`, `query`, `remove`. --- diff --git a/examples/excel/README.md b/examples/excel/README.md deleted file mode 100644 index 05b31adee..000000000 --- a/examples/excel/README.md +++ /dev/null @@ -1,206 +0,0 @@ -# Excel (.xlsx) Examples - -Examples demonstrating OfficeCLI capabilities for Excel spreadsheet automation. - -## 📊 Scripts - -### [gen-beautiful-charts.sh](gen-beautiful-charts.sh) -**Create professional charts with custom styling** - -```bash -bash gen-beautiful-charts.sh -``` - -**Demonstrates:** -- Multiple chart types (bar, line, pie, scatter, area) -- Chart styling and colors -- Data series configuration -- Legend and axis formatting -- Chart positioning - -**Output:** [`outputs/beautiful_charts.xlsx`](outputs/beautiful_charts.xlsx) - ---- - -### [gen-charts-demo.sh](gen-charts-demo.sh) -**Comprehensive chart examples** - -```bash -bash gen-charts-demo.sh -``` - -**Demonstrates:** -- 14+ chart types -- Chart variations (stacked, clustered, 3D) -- Data range selection -- Title and label configuration -- Chart layout - -**Output:** [`outputs/charts_demo.xlsx`](outputs/charts_demo.xlsx) - ---- - -## 📈 Sample Output - -[`outputs/sales_report.xlsx`](outputs/sales_report.xlsx) - Pre-generated sales report example - ---- - -## 🎓 Key Concepts - -### Workbook Structure -``` -/Workbook - /Sheet1 - /A1 # Cell A1 - /B2 # Cell B2 - /A1:C10 # Range A1 to C10 - /Sheet2 - /Chart1 # Chart objects -``` - -### Common Commands - -**Set cell value:** -```bash -officecli set data.xlsx /Sheet1/A1 \ - --prop value="Revenue" \ - --prop bold=true \ - --prop size=14 -``` - -**Add formula:** -```bash -officecli set data.xlsx /Sheet1/B10 \ - --prop formula="=SUM(B2:B9)" \ - --prop numFmt="$#,##0.00" -``` - -**Create chart:** -```bash -officecli add data.xlsx /Sheet1 --type chart \ - --prop chartType=column \ - --prop dataRange="A1:B10" \ - --prop title="Sales Report" -``` - -**Add sheet:** -```bash -officecli add data.xlsx / --type sheet \ - --prop name="Q2 Data" -``` - ---- - -## 📊 Chart Types - -### Supported Chart Types - -| Type | Description | Usage | -|------|-------------|-------| -| `column` | Vertical bar chart | Comparing values | -| `bar` | Horizontal bar chart | Ranking data | -| `line` | Line chart | Trends over time | -| `pie` | Pie chart | Part-to-whole | -| `scatter` | Scatter plot | Correlation | -| `area` | Area chart | Volume over time | -| `doughnut` | Doughnut chart | Part-to-whole with center | -| `radar` | Radar chart | Multivariate data | -| `combo` | Combination chart | Multiple series types | - -**Variations:** -- `columnStacked` - Stacked columns -- `columnClustered` - Grouped columns -- `column3D` - 3D columns -- `lineMarkers` - Line with data points -- `areaStacked` - Stacked areas - -**View all chart types:** -```bash -officecli xlsx add -``` - ---- - -## 📊 Available Properties - -### Cell -- `value` - Cell value (text or number) -- `formula` - Excel formula (e.g., =SUM(A1:A10)) -- `bold` - true/false -- `italic` - true/false -- `size` - Font size -- `font` - Font name -- `color` - Text color (hex) -- `fill` - Background color (hex) -- `numFmt` - Number format (e.g., "0.00%", "$#,##0.00") -- `alignment` - horizontal, vertical -- `border` - Border style - -### Chart -- `chartType` - Chart type (column, bar, line, pie, etc.) -- `dataRange` - Data range (e.g., "A1:B10") -- `title` - Chart title -- `x` - X position -- `y` - Y position -- `width` - Chart width -- `height` - Chart height -- `legend` - Legend position (top, bottom, left, right, none) - -### Sheet -- `name` - Sheet name -- `tabColor` - Tab color (hex) -- `hidden` - true/false - -**For complete property list:** -```bash -officecli xlsx set -officecli xlsx set cell -officecli xlsx set chart -``` - ---- - -## 🔧 Tips - -1. **View data:** - ```bash - officecli view data.xlsx text --cols A,B,C --max-lines 50 - ``` - -2. **Check formulas:** - ```bash - officecli view data.xlsx issues --type content - ``` - -3. **Query cells:** - ```bash - officecli query data.xlsx "cell[formula~=SUM]" - ``` - -4. **Batch cell updates:** - ```bash - cat << EOF | officecli batch data.xlsx - [ - {"command":"set","path":"/Sheet1/A1","props":{"value":"Name","bold":"true"}}, - {"command":"set","path":"/Sheet1/B1","props":{"value":"Score","bold":"true"}}, - {"command":"set","path":"/Sheet1/A2","props":{"value":"Alice"}}, - {"command":"set","path":"/Sheet1/B2","props":{"value":"95"}} - ] - EOF - ``` - -5. **Number formats:** - - Currency: `"$#,##0.00"` - - Percentage: `"0.00%"` - - Date: `"yyyy-mm-dd"` - - Custom: `"#,##0.00;[Red]-#,##0.00"` - ---- - -## 📚 More Resources - -- [Complete Excel documentation](../../SKILL.md#excel-xlsx) -- [All examples](../) -- [Word examples](../word/) -- [PowerPoint examples](../powerpoint/) diff --git a/examples/excel/charts.md b/examples/excel/charts.md new file mode 100644 index 000000000..67fd7295b --- /dev/null +++ b/examples/excel/charts.md @@ -0,0 +1,6 @@ +# charts + +TODO: rewrite script with high-level chart API, add annotated officecli commands. + +See [charts.sh](charts.sh) and [charts.xlsx](charts.xlsx). + diff --git a/examples/excel/gen-beautiful-charts.sh b/examples/excel/charts.sh similarity index 99% rename from examples/excel/gen-beautiful-charts.sh rename to examples/excel/charts.sh index e517bf3eb..f62f664c0 100755 --- a/examples/excel/gen-beautiful-charts.sh +++ b/examples/excel/charts.sh @@ -5,7 +5,7 @@ set -e -XLSX="beautiful_charts.xlsx" +XLSX="$(dirname "$0")/charts.xlsx" echo "" echo "==========================================" echo "Generating beautiful charts document: $XLSX" diff --git a/examples/excel/charts.xlsx b/examples/excel/charts.xlsx new file mode 100644 index 000000000..bb0a05245 Binary files /dev/null and b/examples/excel/charts.xlsx differ diff --git a/examples/excel/charts/charts-advanced.md b/examples/excel/charts/charts-advanced.md new file mode 100644 index 000000000..dea669c36 --- /dev/null +++ b/examples/excel/charts/charts-advanced.md @@ -0,0 +1,151 @@ +# Advanced Charts Showcase + +This demo consists of three files that work together: + +- **charts-advanced.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-advanced.xlsx** — The generated workbook with 3 sheets (12 charts total). +- **charts-advanced.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-advanced.py +# → charts-advanced.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Scatter & Bubble + +Four charts covering scatter plot and bubble chart fundamentals. + +```bash +# Scatter with circle markers and connecting lines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop categories=1,2,3,4,5,6 \ + --prop series1="SeriesA:10,25,15,40,30,50" \ + --prop series2="SeriesB:5,18,22,35,28,42" \ + --prop colors=4472C4,ED7D31 \ + --prop marker=circle --prop markerSize=8 \ + --prop lineWidth=1.5 --prop legend=bottom + +# Scatter with smooth curve and reference line +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop smooth=true --prop marker=diamond --prop markerSize=7 \ + --prop referenceLine=25:FF0000:Target:dash \ + --prop axisTitle=Value --prop catTitle=Period + +# Scatter with per-series marker styles +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop series1.marker=square --prop series2.marker=triangle \ + --prop series3.marker=star --prop markerSize=9 \ + --prop lineWidth=1 --prop gridlines=D9D9D9:0.5:dot + +# Bubble chart with scale control +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop bubbleScale=80 --prop legend=right \ + --prop axisTitle=Revenue --prop catTitle=Market Size +``` + +**Features:** `scatter`, `bubble`, `marker` (circle, diamond, square, triangle, star), `markerSize`, `series{N}.marker` (per-series), `smooth`, `lineWidth`, `referenceLine`, `bubbleScale`, `catTitle`, `axisTitle`, `gridlines`, `legend` + +### Sheet: 2-Combo & Radar + +Four charts covering combo (bar+line) and radar (spider) charts. + +```bash +# Combo chart with comboSplit (bar+line split) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop comboSplit=2 \ + --prop series1="Revenue:120,145,132,168,155,180" \ + --prop series2="Expenses:80,92,85,98,90,105" \ + --prop series3="Growth:8,12,6,15,10,16" \ + --prop legend=bottom --prop axisTitle=Amount --prop catTitle=Month + +# Combo with secondary axis +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop comboSplit=1 --prop secondaryAxis=2 \ + --prop series1="Volume:1200,1450,1320,1680" \ + --prop series2="AvgPrice:45,52,48,58" + +# Combo with per-series type control (combotypes) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop combotypes=column,column,line,area + +# Radar chart with radarStyle=marker +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=marker \ + --prop categories=Speed,Strength,Stamina,Agility,Accuracy \ + --prop series1="AthleteA:80,65,90,75,85" \ + --prop series2="AthleteB:70,85,60,90,70" +``` + +**Features:** `combo`, `comboSplit` (bar/line split point), `combotypes` (per-series type: column/line/area), `secondaryAxis`, `radar`, `radarStyle` (marker/filled/standard), `categories` as spoke labels + +### Sheet: 3-Stock & Radar + +Four charts covering stock (OHLC) and additional radar/bubble variants. + +```bash +# Stock OHLC chart with 4 series (Open/High/Low/Close) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop categories=Mon,Tue,Wed,Thu,Fri \ + --prop series1="Open:145,148,150,147,152" \ + --prop series2="High:152,155,157,153,160" \ + --prop series3="Low:143,146,148,144,150" \ + --prop series4="Close:148,150,147,152,158" \ + --prop catTitle=Day --prop axisTitle=Price + +# Stock chart — weekly OHLC with gridlines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop gridlines=E0E0E0:0.75 + +# Radar — filled style with transparency +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=filled \ + --prop transparency=40 --prop legend=bottom + +# Bubble with single series and axis titles +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop bubbleScale=100 --prop legend=none \ + --prop axisTitle=Revenue --prop catTitle=Market Size +``` + +**Features:** `stock` (OHLC format: 4 series = Open/High/Low/Close), `radarStyle=filled`, `transparency` (fill alpha on radar), `bubbleScale=100`, `legend=none`, `gridlines` styling + +## Complete Feature Coverage + +| Feature | Sheet | +|---------|-------| +| **Chart types:** scatter, bubble, combo, radar, stock | 1, 2, 3 | +| **Scatter:** marker styles, smooth, lineWidth | 1 | +| **Bubble:** bubbleScale, single/multi-series | 1, 3 | +| **Combo:** comboSplit, combotypes, secondaryAxis | 2 | +| **Radar:** radarStyle (marker, filled, standard), transparency | 2, 3 | +| **Stock:** OHLC (4 series), gridlines | 3 | +| **Markers:** circle, diamond, square, triangle, star, per-series | 1 | +| **Data input:** inline series, categories | 1, 2, 3 | +| **Axis:** catTitle, axisTitle | 1, 2, 3 | +| **Legend:** position (bottom, right, none) | 1, 2, 3 | +| **Reference line:** value:color:label:dash | 1 | +| **Gridlines:** color:width:dash | 1, 3 | + +## Inspect the Generated File + +```bash +officecli query charts-advanced.xlsx chart +officecli get charts-advanced.xlsx "/1-Scatter & Bubble/chart[1]" +``` diff --git a/examples/excel/charts/charts-advanced.py b/examples/excel/charts/charts-advanced.py new file mode 100644 index 000000000..08fa4b698 --- /dev/null +++ b/examples/excel/charts/charts-advanced.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +""" +Advanced Charts Showcase — scatter, bubble, combo, radar, and stock charts. + +Generates: charts-advanced.xlsx + +Usage: + python3 charts-advanced.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-advanced.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Sheet: 1-Scatter & Bubble +# ========================================================================== +print("\n--- 1-Scatter & Bubble ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Scatter & Bubble"') + +# -------------------------------------------------------------------------- +# Chart 1: Scatter with markers — circle markers, line connecting points +# +# officecli add charts-advanced.xlsx "/1-Scatter & Bubble" --type chart \ +# --prop chartType=scatter \ +# --prop title="Scatter: Markers & Line" \ +# --prop categories=1,2,3,4,5,6 \ +# --prop series1="SeriesA:10,25,15,40,30,50" \ +# --prop series2="SeriesB:5,18,22,35,28,42" \ +# --prop colors=4472C4,ED7D31 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop marker=circle --prop markerSize=8 \ +# --prop lineWidth=1.5 \ +# --prop legend=bottom +# +# Features: chartType=scatter, categories as X values, marker=circle, +# markerSize, lineWidth, legend=bottom +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Scatter & Bubble" --type chart' + f' --prop chartType=scatter' + f' --prop title="Scatter: Markers & Line"' + f' --prop categories=1,2,3,4,5,6' + f' --prop series1=SeriesA:10,25,15,40,30,50' + f' --prop series2=SeriesB:5,18,22,35,28,42' + f' --prop colors=4472C4,ED7D31' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop marker=circle --prop markerSize=8' + f' --prop lineWidth=1.5' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: Scatter with smooth curve and trendline (reference line) +# +# officecli add charts-advanced.xlsx "/1-Scatter & Bubble" --type chart \ +# --prop chartType=scatter \ +# --prop title="Scatter: Smooth + Trendline" \ +# --prop categories=1,2,3,4,5,6,7,8 \ +# --prop series1="Growth:3,7,12,20,28,35,40,45" \ +# --prop colors=70AD47 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop smooth=true \ +# --prop marker=diamond --prop markerSize=7 \ +# --prop referenceLine=25:FF0000:Target:dash \ +# --prop axisTitle=Value --prop catTitle=Period +# +# Features: smooth=true (smooth curve), marker=diamond, +# referenceLine (trendline overlay), axisTitle, catTitle +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Scatter & Bubble" --type chart' + f' --prop chartType=scatter' + f' --prop title="Scatter: Smooth + Trendline"' + f' --prop categories=1,2,3,4,5,6,7,8' + f' --prop series1=Growth:3,7,12,20,28,35,40,45' + f' --prop colors=70AD47' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop smooth=true' + f' --prop marker=diamond --prop markerSize=7' + f' --prop referenceLine=25:FF0000:Target:dash' + f' --prop axisTitle=Value --prop catTitle=Period') + +# -------------------------------------------------------------------------- +# Chart 3: Scatter with varied marker styles per series +# +# officecli add charts-advanced.xlsx "/1-Scatter & Bubble" --type chart \ +# --prop chartType=scatter \ +# --prop title="Scatter: Marker Styles" \ +# --prop categories=10,20,30,40,50 \ +# --prop series1="Squares:8,22,18,35,30" \ +# --prop series2="Triangles:15,10,28,20,42" \ +# --prop series3="Stars:5,30,12,45,25" \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop series1.marker=square \ +# --prop series2.marker=triangle \ +# --prop series3.marker=star \ +# --prop markerSize=9 \ +# --prop lineWidth=1 \ +# --prop gridlines=D9D9D9:0.5:dot +# +# Features: per-series marker style (series{N}.marker), gridlines styling +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Scatter & Bubble" --type chart' + f' --prop chartType=scatter' + f' --prop title="Scatter: Marker Styles"' + f' --prop categories=10,20,30,40,50' + f' --prop series1=Squares:8,22,18,35,30' + f' --prop series2=Triangles:15,10,28,20,42' + f' --prop series3=Stars:5,30,12,45,25' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop series1.marker=square' + f' --prop series2.marker=triangle' + f' --prop series3.marker=star' + f' --prop markerSize=9' + f' --prop lineWidth=1' + f' --prop gridlines=D9D9D9:0.5:dot') + +# -------------------------------------------------------------------------- +# Chart 4: Bubble chart with size data +# +# officecli add charts-advanced.xlsx "/1-Scatter & Bubble" --type chart \ +# --prop chartType=bubble \ +# --prop title="Bubble: Market Size" \ +# --prop categories=10,25,40,60,80 \ +# --prop series1="ProductA:30,50,20,70,45" \ +# --prop series2="ProductB:15,35,55,40,60" \ +# --prop colors=4472C4,ED7D31 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop bubbleScale=80 \ +# --prop legend=right \ +# --prop dataLabels=false +# +# Features: chartType=bubble, categories as X, series as Y values, +# bubble sizes default to Y values, bubbleScale to control sizing +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Scatter & Bubble" --type chart' + f' --prop chartType=bubble' + f' --prop title="Bubble: Market Size"' + f' --prop categories=10,25,40,60,80' + f' --prop series1=ProductA:30,50,20,70,45' + f' --prop series2=ProductB:15,35,55,40,60' + f' --prop colors=4472C4,ED7D31' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop bubbleScale=80' + f' --prop legend=right') + +# ========================================================================== +# Sheet: 2-Combo & Radar +# ========================================================================== +print("\n--- 2-Combo & Radar ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Combo & Radar"') + +# -------------------------------------------------------------------------- +# Chart 1: Combo chart — bar+line with comboSplit +# +# officecli add charts-advanced.xlsx "/2-Combo & Radar" --type chart \ +# --prop chartType=combo \ +# --prop title="Combo: Sales (Bar) + Growth % (Line)" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop series1="Revenue:120,145,132,168,155,180" \ +# --prop series2="Expenses:80,92,85,98,90,105" \ +# --prop series3="Growth:8,12,6,15,10,16" \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop comboSplit=2 \ +# --prop legend=bottom \ +# --prop axisTitle=Amount --prop catTitle=Month +# +# Features: chartType=combo, comboSplit=2 (first 2 series as bars, +# remaining as lines), categories as X labels +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Combo & Radar" --type chart' + f' --prop chartType=combo' + f' --prop title="Combo: Sales (Bar) + Growth % (Line)"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop series1=Revenue:120,145,132,168,155,180' + f' --prop series2=Expenses:80,92,85,98,90,105' + f' --prop series3=Growth:8,12,6,15,10,16' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop comboSplit=2' + f' --prop legend=bottom' + f' --prop axisTitle=Amount --prop catTitle=Month') + +# -------------------------------------------------------------------------- +# Chart 2: Combo with secondary axis +# +# officecli add charts-advanced.xlsx "/2-Combo & Radar" --type chart \ +# --prop chartType=combo \ +# --prop title="Combo: Volume (Bar) + Price (Line, 2nd Axis)" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop series1="Volume:1200,1450,1320,1680" \ +# --prop series2="AvgPrice:45,52,48,58" \ +# --prop colors=5B9BD5,FF0000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop comboSplit=1 \ +# --prop secondaryAxis=2 \ +# --prop legend=bottom +# +# Features: comboSplit=1, secondaryAxis=2 (series 2 on right Y-axis) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Combo & Radar" --type chart' + f' --prop chartType=combo' + f' --prop title="Combo: Volume (Bar) + Price (Line, 2nd Axis)"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop series1=Volume:1200,1450,1320,1680' + f' --prop series2=AvgPrice:45,52,48,58' + f' --prop colors=5B9BD5,FF0000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop comboSplit=1' + f' --prop secondaryAxis=2' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Combo with combotypes — per-series type control +# +# officecli add charts-advanced.xlsx "/2-Combo & Radar" --type chart \ +# --prop chartType=combo \ +# --prop title="Combo: Mixed Types (combotypes)" \ +# --prop categories=A,B,C,D,E \ +# --prop series1="Bars:30,45,28,52,40" \ +# --prop series2="MoreBars:20,30,22,38,28" \ +# --prop series3="Lines:12,18,15,22,16" \ +# --prop series4="Area:8,12,10,15,11" \ +# --prop colors=4472C4,5B9BD5,ED7D31,70AD47 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop combotypes=column,column,line,area \ +# --prop legend=bottom +# +# Features: combotypes (per-series type: column, column, line, area) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Combo & Radar" --type chart' + f' --prop chartType=combo' + f' --prop title="Combo: Mixed Types (combotypes)"' + f' --prop categories=A,B,C,D,E' + f' --prop series1=Bars:30,45,28,52,40' + f' --prop series2=MoreBars:20,30,22,38,28' + f' --prop series3=Lines:12,18,15,22,16' + f' --prop series4=Area:8,12,10,15,11' + f' --prop colors=4472C4,5B9BD5,ED7D31,70AD47' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop combotypes=column,column,line,area' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Radar (spider) chart with multiple series +# +# officecli add charts-advanced.xlsx "/2-Combo & Radar" --type chart \ +# --prop chartType=radar \ +# --prop title="Radar: Skills Comparison" \ +# --prop categories=Speed,Strength,Stamina,Agility,Accuracy \ +# --prop series1="AthleteA:80,65,90,75,85" \ +# --prop series2="AthleteB:70,85,60,90,70" \ +# --prop series3="AthleteC:90,70,75,65,80" \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop radarStyle=marker \ +# --prop legend=bottom +# +# Features: chartType=radar, categories as spoke labels, +# multiple series, radarStyle=marker +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Combo & Radar" --type chart' + f' --prop chartType=radar' + f' --prop title="Radar: Skills Comparison"' + f' --prop categories=Speed,Strength,Stamina,Agility,Accuracy' + f' --prop series1=AthleteA:80,65,90,75,85' + f' --prop series2=AthleteB:70,85,60,90,70' + f' --prop series3=AthleteC:90,70,75,65,80' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop radarStyle=marker' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 3-Stock & More Radar +# ========================================================================== +print("\n--- 3-Stock & More Radar ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Stock & Radar"') + +# -------------------------------------------------------------------------- +# Chart 1: Stock (OHLC) chart — Open-High-Low-Close +# +# officecli add charts-advanced.xlsx "/3-Stock & Radar" --type chart \ +# --prop chartType=stock \ +# --prop title="Stock: OHLC Daily Prices" \ +# --prop categories=Mon,Tue,Wed,Thu,Fri \ +# --prop series1="Open:145,148,150,147,152" \ +# --prop series2="High:152,155,157,153,160" \ +# --prop series3="Low:143,146,148,144,150" \ +# --prop series4="Close:148,150,147,152,158" \ +# --prop x=0 --prop y=0 --prop width=14 --prop height=18 \ +# --prop legend=bottom \ +# --prop catTitle=Day --prop axisTitle=Price +# +# Features: chartType=stock, 4 series (Open/High/Low/Close), +# categories as date labels, catTitle, axisTitle +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Stock & Radar" --type chart' + f' --prop chartType=stock' + f' --prop title="Stock: OHLC Daily Prices"' + f' --prop categories=Mon,Tue,Wed,Thu,Fri' + f' --prop series1=Open:145,148,150,147,152' + f' --prop series2=High:152,155,157,153,160' + f' --prop series3=Low:143,146,148,144,150' + f' --prop series4=Close:148,150,147,152,158' + f' --prop x=0 --prop y=0 --prop width=14 --prop height=18' + f' --prop legend=bottom' + f' --prop catTitle=Day --prop axisTitle=Price') + +# -------------------------------------------------------------------------- +# Chart 2: Stock chart — weekly OHLC with date categories +# +# officecli add charts-advanced.xlsx "/3-Stock & Radar" --type chart \ +# --prop chartType=stock \ +# --prop title="Stock: Weekly OHLC (6 Weeks)" \ +# --prop categories=W1,W2,W3,W4,W5,W6 \ +# --prop series1="Open:100,104,102,108,105,110" \ +# --prop series2="High:106,110,108,115,112,118" \ +# --prop series3="Low:98,101,100,105,103,107" \ +# --prop series4="Close:104,102,108,105,110,115" \ +# --prop x=15 --prop y=0 --prop width=14 --prop height=18 \ +# --prop gridlines=E0E0E0:0.75 \ +# --prop legend=bottom +# +# Features: stock chart with 6 weeks of OHLC, gridlines styling +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Stock & Radar" --type chart' + f' --prop chartType=stock' + f' --prop title="Stock: Weekly OHLC (6 Weeks)"' + f' --prop categories=W1,W2,W3,W4,W5,W6' + f' --prop series1=Open:100,104,102,108,105,110' + f' --prop series2=High:106,110,108,115,112,118' + f' --prop series3=Low:98,101,100,105,103,107' + f' --prop series4=Close:104,102,108,105,110,115' + f' --prop x=15 --prop y=0 --prop width=14 --prop height=18' + f' --prop gridlines=E0E0E0:0.75' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Radar — filled style (spider web) +# +# officecli add charts-advanced.xlsx "/3-Stock & Radar" --type chart \ +# --prop chartType=radar \ +# --prop title="Radar: Product Ratings (Filled)" \ +# --prop categories=Quality,Price,Design,Support,Delivery \ +# --prop series1="BrandX:85,70,90,75,80" \ +# --prop series2="BrandY:70,90,65,85,75" \ +# --prop colors=4472C4,70AD47 \ +# --prop x=0 --prop y=19 --prop width=14 --prop height=18 \ +# --prop radarStyle=filled \ +# --prop transparency=40 \ +# --prop legend=bottom +# +# Features: radarStyle=filled, transparency (fill alpha), multiple series +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Stock & Radar" --type chart' + f' --prop chartType=radar' + f' --prop title="Radar: Product Ratings (Filled)"' + f' --prop categories=Quality,Price,Design,Support,Delivery' + f' --prop series1=BrandX:85,70,90,75,80' + f' --prop series2=BrandY:70,90,65,85,75' + f' --prop colors=4472C4,70AD47' + f' --prop x=0 --prop y=19 --prop width=14 --prop height=18' + f' --prop radarStyle=filled' + f' --prop transparency=40' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Bubble — single series with explicit large differences in size +# +# officecli add charts-advanced.xlsx "/3-Stock & Radar" --type chart \ +# --prop chartType=bubble \ +# --prop title="Bubble: Regional Opportunity" \ +# --prop categories=5,15,30,50,70,90 \ +# --prop series1="Regions:20,45,30,80,55,65" \ +# --prop colors=4472C4 \ +# --prop x=15 --prop y=19 --prop width=14 --prop height=18 \ +# --prop bubbleScale=100 \ +# --prop legend=none \ +# --prop axisTitle=Revenue --prop catTitle=Market Size +# +# Features: bubble with single series, bubbleScale=100, legend=none, +# axisTitle and catTitle labels +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Stock & Radar" --type chart' + f' --prop chartType=bubble' + f' --prop title="Bubble: Regional Opportunity"' + f' --prop categories=5,15,30,50,70,90' + f' --prop series1=Regions:20,45,30,80,55,65' + f' --prop colors=4472C4' + f' --prop x=15 --prop y=19 --prop width=14 --prop height=18' + f' --prop bubbleScale=100' + f' --prop legend=none' + f' --prop axisTitle=Revenue --prop catTitle=Market Size') + +print(f"\nDone! Generated: {FILE}") +print(" 3 sheets (1-Scatter & Bubble, 2-Combo & Radar, 3-Stock & Radar)") +print(" 12 charts total: scatter(3), bubble(2), combo(3), radar(2), stock(2)") diff --git a/examples/excel/charts/charts-advanced.xlsx b/examples/excel/charts/charts-advanced.xlsx new file mode 100644 index 000000000..21aa0c4e7 Binary files /dev/null and b/examples/excel/charts/charts-advanced.xlsx differ diff --git a/examples/excel/charts/charts-area.md b/examples/excel/charts/charts-area.md new file mode 100644 index 000000000..f4337ae95 --- /dev/null +++ b/examples/excel/charts/charts-area.md @@ -0,0 +1,220 @@ +# Area Charts Showcase + +This demo consists of three files that work together: + +- **charts-area.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-area.xlsx** — The generated workbook with 6 sheets (1 data + 5 chart sheets, 20 charts total). +- **charts-area.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-area.py +# → charts-area.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Area Fundamentals + +Four area charts covering data input methods, transparency, area fills, and gradients. + +```bash +# Basic area with dataRange and axis titles +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop dataRange=Sheet1!A1:E13 \ + --prop colors=4472C4,ED7D31,70AD47,FFC000 \ + --prop catTitle=Month --prop axisTitle=Visitors \ + --prop gridlines=D9D9D9:0.5:dot + +# Inline series with transparency +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop series1="Subscriptions:120,180,210,250" \ + --prop series2="One-time:90,140,160,200" \ + --prop transparency=40 --prop legend=bottom + +# Area with areafill gradient (single series) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop series1="Users:3200,3800,4500,5100,5800,6400" \ + --prop areafill=4472C4-BDD7EE:90 --prop legend=none + +# Per-series gradient fills +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop 'gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90' \ + --prop legend=right --prop legendfont=10:333333:Calibri +``` + +**Features:** `area`, `dataRange`, `categories`, `colors`, `catTitle`, `axisTitle`, `gridlines`, `transparency`, `areafill` (gradient from-to:angle), `gradients` (per-series), `legend` (bottom, right, none), `legendfont` + +### Sheet: 2-Area Variants + +Four charts covering all area chart type variants — stacked, percent stacked, and 3D. + +```bash +# Stacked area with solid plot fill +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=areaStacked \ + --prop plotFill=F5F5F5 --prop roundedCorners=true + +# 100% stacked area with axis number format +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=areaPercentStacked \ + --prop axisNumFmt=0% --prop axisLine=333333:1:solid + +# 3D area with perspective rotation +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area3d \ + --prop view3d=20,25,15 + +# 3D area with multiple series and gridlines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area3d \ + --prop view3d=15,20,20 --prop gridlines=D9D9D9:0.5:dot +``` + +**Features:** `areaStacked`, `areaPercentStacked`, `area3d`, `plotFill` (solid), `roundedCorners`, `axisNumFmt`, `axisLine`, `view3d` (rotX,rotY,perspective) + +### Sheet: 3-Area Styling + +Four charts demonstrating visual styling — title effects, shadows, gridlines, and fills. + +```bash +# Title styling with shadow +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop title.font=Georgia --prop title.size=16 \ + --prop title.color=1F4E79 --prop title.bold=true \ + --prop title.shadow=000000-3-315-2-30 + +# Series shadow, outline, and smooth curve +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop smooth=true \ + --prop series.shadow=000000-4-315-2-40 \ + --prop series.outline=333333-1 + +# Axis font with gridlines and minor gridlines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop axisfont=9:58626E:Arial \ + --prop gridlines=D9D9D9:0.5:dot \ + --prop minorGridlines=EEEEEE:0.3:dot + +# Chart fill, plot fill gradient, and borders +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop chartFill=FAFAFA \ + --prop plotFill=E8F0FE-D6E4F0:90 \ + --prop chartArea.border=D0D0D0:1:solid \ + --prop plotArea.border=E0E0E0:0.5:dot +``` + +**Features:** `title.font`/`.size`/`.color`/`.bold`/`.shadow`, `smooth`, `series.shadow` (color-blur-angle-dist-opacity), `series.outline` (color-width), `axisfont` (size:color:font), `gridlines`, `minorGridlines`, `chartFill`, `plotFill` (gradient), `chartArea.border`, `plotArea.border`, `roundedCorners` + +### Sheet: 4-Labels & Legend + +Four charts demonstrating data label and legend customization plus manual layout. + +```bash +# Data labels with position, font, and number format +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop dataLabels=true --prop labelPos=top \ + --prop labelFont=9:333333:true \ + --prop dataLabels.numFmt=#,##0 + +# Individual label deletion and per-point colors +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop dataLabels=true \ + --prop dataLabel1.delete=true --prop dataLabel2.delete=true \ + --prop point4.color=C00000 + +# Legend overlay with font styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop legend=right --prop legendfont=10:1F4E79:Calibri \ + --prop legend.overlay=true + +# Manual layout — plotArea, title, legend positioning +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop plotArea.x=0.12 --prop plotArea.y=0.18 \ + --prop plotArea.w=0.82 --prop plotArea.h=0.55 \ + --prop title.x=0.25 --prop title.y=0.02 \ + --prop legend.x=0.15 --prop legend.y=0.82 \ + --prop legend.w=0.7 --prop legend.h=0.12 +``` + +**Features:** `dataLabels`, `labelPos` (top), `labelFont`, `dataLabels.numFmt`, `dataLabel{N}.delete`, `point{N}.color`, `legend` (right), `legendfont`, `legend.overlay`, `plotArea.x/y/w/h`, `title.x/y`, `legend.x/y/w/h` + +### Sheet: 5-Advanced + +Four charts demonstrating advanced features — secondary axis, reference lines, axis scaling, and effects. + +```bash +# Secondary axis (dual scale) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop secondaryAxis=2 \ + --prop series1="Revenue:120,180,250,310,280,340" \ + --prop series2="Conv %:2.1,2.8,3.2,3.9,3.5,4.1" + +# Reference line (target/threshold) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop referenceLine=100:FF0000:1.5:dash \ + --prop areafill=4472C4-BDD7EE:90 + +# Axis scaling with display units +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop axisMin=3000 --prop axisMax=7000 \ + --prop majorUnit=500 --prop dispUnits=thousands + +# Color rule with title glow and series shadow +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop colorRule=50:C00000:70AD47 \ + --prop referenceLine=50:888888:1:solid \ + --prop title.glow=4472C4-8-60 \ + --prop series.shadow=000000-3-315-1-30 +``` + +**Features:** `secondaryAxis` (1-based series index), `referenceLine` (value:color:width:dash), `axisMin`, `axisMax`, `majorUnit`, `dispUnits` (thousands), `colorRule` (threshold:belowColor:aboveColor), `title.glow` (color-radius-opacity), `areafill` + +## Complete Feature Coverage + +| Feature | Sheet | +|---------|-------| +| **Chart types:** area, areaStacked, areaPercentStacked, area3d | 1, 2 | +| **Data input:** dataRange, series, categories, colors | 1 | +| **Area fills:** areafill (gradient), gradients (per-series), transparency | 1, 5 | +| **Axis titles:** catTitle, axisTitle | 1, 3 | +| **Axis scaling:** axisMin/Max, majorUnit, dispUnits | 5 | +| **Axis features:** axisNumFmt, axisLine | 2 | +| **Gridlines:** gridlines, minorGridlines | 1, 3 | +| **Data labels:** dataLabels, labelPos, labelFont, numFmt | 4 | +| **Custom labels:** dataLabel{N}.delete | 4 | +| **Point color:** point{N}.color | 4 | +| **Legend:** position, legendfont, legend.overlay, legend=none | 1, 4 | +| **Layout:** plotArea.x/y/w/h, title.x/y, legend.x/y/w/h | 4 | +| **Effects:** series.shadow, series.outline, smooth | 3 | +| **Title styling:** font, size, color, bold, shadow, glow | 3, 5 | +| **Fills:** plotFill, chartFill (solid + gradient) | 2, 3 | +| **Borders:** chartArea.border, plotArea.border | 3 | +| **Advanced:** secondaryAxis, referenceLine, colorRule | 5 | +| **3D:** view3d | 2 | +| **Other:** roundedCorners | 2, 3 | + +## Inspect the Generated File + +```bash +officecli query charts-area.xlsx chart +officecli get charts-area.xlsx "/1-Area Fundamentals/chart[1]" +``` diff --git a/examples/excel/charts/charts-area.py b/examples/excel/charts/charts-area.py new file mode 100755 index 000000000..63937b516 --- /dev/null +++ b/examples/excel/charts/charts-area.py @@ -0,0 +1,667 @@ +#!/usr/bin/env python3 +""" +Area Charts Showcase — area, areaStacked, areaPercentStacked, and area3d with all variations. + +Generates: charts-area.xlsx + +Every area chart feature officecli supports is demonstrated at least once: +area fills, gradients, transparency, stacking, axis scaling, gridlines, +data labels, legend positioning, reference lines, secondary axis, +shadows, manual layout, and 3D rotation. + +5 sheets, 20 charts total. + + 1-Area Fundamentals 4 charts — data input variants, transparency, area fills, gradients + 2-Area Variants 4 charts — areaStacked, areaPercentStacked, area3d + 3-Area Styling 4 charts — title styling, shadows, gridlines, chart/plot fills + 4-Labels & Legend 4 charts — data labels, per-point colors, legend, manual layout + 5-Advanced 4 charts — secondary axis, reference line, axis scaling, effects + +Usage: + python3 charts-area.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-area.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Source data — shared across all charts +# ========================================================================== +print("\n--- Populating source data ---") + +data_cmds = [] +for j, h in enumerate(["Month", "Organic", "Paid", "Social", "Referral"]): + data_cmds.append({"command": "set", "path": f"/Sheet1/{'ABCDE'[j]}1", "props": {"text": h, "bold": "true"}}) + +months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"] +organic = [4200, 4800, 5100, 5600, 6200, 6800, 7500, 8100, 7600, 7200, 6900, 7800] +paid = [3100, 3500, 3800, 4200, 4800, 5200, 5800, 6300, 5900, 5500, 5100, 5700] +social = [1800, 2100, 2400, 2800, 3200, 3600, 4000, 4300, 3900, 3500, 3200, 3800] +referral = [1200, 1400, 1500, 1700, 1900, 2100, 2300, 2500, 2300, 2100, 1900, 2200] + +for i in range(12): + r = i + 2 + for j, val in enumerate([months[i], organic[i], paid[i], social[i], referral[i]]): + data_cmds.append({"command": "set", "path": f"/Sheet1/{'ABCDE'[j]}{r}", "props": {"text": str(val)}}) + +cli(f'batch "{FILE}" --force --commands \'{json.dumps(data_cmds)}\'') + +# ========================================================================== +# Sheet: 1-Area Fundamentals +# ========================================================================== +print("\n--- 1-Area Fundamentals ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Area Fundamentals"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic area chart with dataRange, axis titles, and custom colors +# +# officecli add charts-area.xlsx "/1-Area Fundamentals" --type chart \ +# --prop chartType=area \ +# --prop title="Website Traffic Overview" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop catTitle=Month --prop axisTitle=Visitors \ +# --prop gridlines=D9D9D9:0.5:dot +# +# Features: chartType=area, dataRange, colors, catTitle, axisTitle, gridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Area Fundamentals" --type chart' + f' --prop chartType=area' + f' --prop title="Website Traffic Overview"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop catTitle=Month --prop axisTitle=Visitors' + f' --prop gridlines=D9D9D9:0.5:dot') + +# -------------------------------------------------------------------------- +# Chart 2: Inline series with transparency +# +# officecli add charts-area.xlsx "/1-Area Fundamentals" --type chart \ +# --prop chartType=area \ +# --prop title="Quarterly Revenue Streams" \ +# --prop series1="Subscriptions:120,180,210,250" \ +# --prop series2="One-time:90,140,160,200" \ +# --prop series3="Services:60,85,110,145" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop colors=2E75B6,70AD47,FFC000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop transparency=40 \ +# --prop legend=bottom +# +# Features: inline series, transparency (0-100), legend=bottom +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Area Fundamentals" --type chart' + f' --prop chartType=area' + f' --prop title="Quarterly Revenue Streams"' + f' --prop series1="Subscriptions:120,180,210,250"' + f' --prop series2="One-time:90,140,160,200"' + f' --prop series3="Services:60,85,110,145"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop colors=2E75B6,70AD47,FFC000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop transparency=40' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Area with areafill gradient +# +# officecli add charts-area.xlsx "/1-Area Fundamentals" --type chart \ +# --prop chartType=area \ +# --prop title="Monthly Active Users" \ +# --prop series1="Users:3200,3800,4500,5100,5800,6400" \ +# --prop categories=Jul,Aug,Sep,Oct,Nov,Dec \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop areafill=4472C4-BDD7EE:90 \ +# --prop legend=none +# +# Features: areafill (gradient from-to:angle), legend=none, single series +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Area Fundamentals" --type chart' + f' --prop chartType=area' + f' --prop title="Monthly Active Users"' + f' --prop series1="Users:3200,3800,4500,5100,5800,6400"' + f' --prop categories=Jul,Aug,Sep,Oct,Nov,Dec' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop areafill=4472C4-BDD7EE:90' + f' --prop legend=none') + +# -------------------------------------------------------------------------- +# Chart 4: Per-series gradient fills +# +# officecli add charts-area.xlsx "/1-Area Fundamentals" --type chart \ +# --prop chartType=area \ +# --prop title="Revenue by Channel" \ +# --prop series1="Direct:45,52,61,70" \ +# --prop series2="Partner:30,38,42,55" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop 'gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90' \ +# --prop legend=right --prop legendfont=10:333333:Calibri +# +# Features: gradients (per-series gradient fills from-to:angle;...), +# legendfont (size:color:font) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Area Fundamentals" --type chart' + f' --prop chartType=area' + f' --prop title="Revenue by Channel"' + f' --prop series1="Direct:45,52,61,70"' + f' --prop series2="Partner:30,38,42,55"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop "gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90"' + f' --prop legend=right --prop legendfont=10:333333:Calibri') + +# ========================================================================== +# Sheet: 2-Area Variants +# ========================================================================== +print("\n--- 2-Area Variants ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Area Variants"') + +# -------------------------------------------------------------------------- +# Chart 1: Stacked area with plotFill and rounded corners +# +# officecli add charts-area.xlsx "/2-Area Variants" --type chart \ +# --prop chartType=areaStacked \ +# --prop title="Cumulative Traffic Sources" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop plotFill=F5F5F5 \ +# --prop roundedCorners=true \ +# --prop legend=bottom +# +# Features: chartType=areaStacked, plotFill (solid), roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Area Variants" --type chart' + f' --prop chartType=areaStacked' + f' --prop title="Cumulative Traffic Sources"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop plotFill=F5F5F5' + f' --prop roundedCorners=true' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: 100% stacked area with axis number format and axis line +# +# officecli add charts-area.xlsx "/2-Area Variants" --type chart \ +# --prop chartType=areaPercentStacked \ +# --prop title="Traffic Share by Channel" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop colors=2E75B6,C55A11,548235,BF8F00 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop axisNumFmt=0% \ +# --prop axisLine=333333:1:solid \ +# --prop legend=bottom +# +# Features: chartType=areaPercentStacked, axisNumFmt, axisLine +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Area Variants" --type chart' + f' --prop chartType=areaPercentStacked' + f' --prop title="Traffic Share by Channel"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop colors=2E75B6,C55A11,548235,BF8F00' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop axisNumFmt=0%' + f' --prop axisLine=333333:1:solid' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: 3D area with perspective rotation +# +# officecli add charts-area.xlsx "/2-Area Variants" --type chart \ +# --prop chartType=area3d \ +# --prop title="3D Regional Sales" \ +# --prop series1="East:120,135,148,162,155,178" \ +# --prop series2="West:95,108,115,128,142,155" \ +# --prop series3="Central:88,92,105,118,125,138" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=20,25,15 \ +# --prop legend=right +# +# Features: chartType=area3d, view3d (rotX,rotY,perspective) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Area Variants" --type chart' + f' --prop chartType=area3d' + f' --prop title="3D Regional Sales"' + f' --prop series1="East:120,135,148,162,155,178"' + f' --prop series2="West:95,108,115,128,142,155"' + f' --prop series3="Central:88,92,105,118,125,138"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=20,25,15' + f' --prop legend=right') + +# -------------------------------------------------------------------------- +# Chart 4: 3D stacked area +# +# officecli add charts-area.xlsx "/2-Area Variants" --type chart \ +# --prop chartType=area3d \ +# --prop title="3D Stacked Inventory" \ +# --prop series1="Warehouse A:500,480,520,550,530,560" \ +# --prop series2="Warehouse B:320,350,340,380,400,410" \ +# --prop series3="Warehouse C:180,200,210,230,250,240" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop colors=1F4E79,2E75B6,9DC3E6 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=15,20,20 \ +# --prop gridlines=D9D9D9:0.5:dot +# +# Features: area3d stacked appearance, multiple series, gridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Area Variants" --type chart' + f' --prop chartType=area3d' + f' --prop title="3D Stacked Inventory"' + f' --prop series1="Warehouse A:500,480,520,550,530,560"' + f' --prop series2="Warehouse B:320,350,340,380,400,410"' + f' --prop series3="Warehouse C:180,200,210,230,250,240"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop colors=1F4E79,2E75B6,9DC3E6' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=15,20,20' + f' --prop gridlines=D9D9D9:0.5:dot') + +# ========================================================================== +# Sheet: 3-Area Styling +# ========================================================================== +print("\n--- 3-Area Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Area Styling"') + +# -------------------------------------------------------------------------- +# Chart 1: Title styling (font, size, color, bold, shadow) +# +# officecli add charts-area.xlsx "/3-Area Styling" --type chart \ +# --prop chartType=area \ +# --prop title="Styled Title Demo" \ +# --prop series1="Revenue:80,120,160,200,240,280" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop colors=4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop title.font=Georgia --prop title.size=16 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop title.shadow=000000-3-315-2-30 \ +# --prop transparency=30 +# +# Features: title.font, title.size, title.color, title.bold, title.shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Area Styling" --type chart' + f' --prop chartType=area' + f' --prop title="Styled Title Demo"' + f' --prop series1="Revenue:80,120,160,200,240,280"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop colors=4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop title.font=Georgia --prop title.size=16' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop title.shadow=000000-3-315-2-30' + f' --prop transparency=30') + +# -------------------------------------------------------------------------- +# Chart 2: Series shadow, outline, and smooth curve +# +# officecli add charts-area.xlsx "/3-Area Styling" --type chart \ +# --prop chartType=area \ +# --prop title="Smooth Area with Effects" \ +# --prop series1="Signups:150,180,220,260,310,350" \ +# --prop series2="Trials:90,110,140,170,200,230" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop colors=4472C4,70AD47 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop smooth=true \ +# --prop series.shadow=000000-4-315-2-40 \ +# --prop series.outline=333333-1 \ +# --prop transparency=25 +# +# Features: smooth, series.shadow (color-blur-angle-dist-opacity), +# series.outline (color-width) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Area Styling" --type chart' + f' --prop chartType=area' + f' --prop title="Smooth Area with Effects"' + f' --prop series1="Signups:150,180,220,260,310,350"' + f' --prop series2="Trials:90,110,140,170,200,230"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop colors=4472C4,70AD47' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop smooth=true' + f' --prop series.shadow=000000-4-315-2-40' + f' --prop series.outline=333333-1' + f' --prop transparency=25') + +# -------------------------------------------------------------------------- +# Chart 3: Axis font styling, gridlines, and minor gridlines +# +# officecli add charts-area.xlsx "/3-Area Styling" --type chart \ +# --prop chartType=area \ +# --prop title="Gridline Configuration" \ +# --prop dataRange=Sheet1!A1:C13 \ +# --prop colors=2E75B6,C55A11 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop axisfont=9:58626E:Arial \ +# --prop gridlines=D9D9D9:0.5:dot \ +# --prop minorGridlines=EEEEEE:0.3:dot \ +# --prop catTitle=Month --prop axisTitle=Visitors +# +# Features: axisfont (size:color:font), gridlines (color:width:dash), +# minorGridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Area Styling" --type chart' + f' --prop chartType=area' + f' --prop title="Gridline Configuration"' + f' --prop dataRange=Sheet1!A1:C13' + f' --prop colors=2E75B6,C55A11' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop axisfont=9:58626E:Arial' + f' --prop gridlines=D9D9D9:0.5:dot' + f' --prop minorGridlines=EEEEEE:0.3:dot' + f' --prop catTitle=Month --prop axisTitle=Visitors') + +# -------------------------------------------------------------------------- +# Chart 4: Chart fill, plot fill gradient, chart/plot area borders +# +# officecli add charts-area.xlsx "/3-Area Styling" --type chart \ +# --prop chartType=area \ +# --prop title="Fills and Borders" \ +# --prop series1="Sales:200,240,280,320,360,400" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop colors=4472C4 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop chartFill=FAFAFA \ +# --prop plotFill=E8F0FE-D6E4F0:90 \ +# --prop chartArea.border=D0D0D0:1:solid \ +# --prop plotArea.border=E0E0E0:0.5:dot \ +# --prop roundedCorners=true +# +# Features: chartFill, plotFill (gradient from-to:angle), +# chartArea.border, plotArea.border, roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Area Styling" --type chart' + f' --prop chartType=area' + f' --prop title="Fills and Borders"' + f' --prop series1="Sales:200,240,280,320,360,400"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop colors=4472C4' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop chartFill=FAFAFA' + f' --prop "plotFill=E8F0FE-D6E4F0:90"' + f' --prop chartArea.border=D0D0D0:1:solid' + f' --prop plotArea.border=E0E0E0:0.5:dot' + f' --prop roundedCorners=true') + +# ========================================================================== +# Sheet: 4-Labels & Legend +# ========================================================================== +print("\n--- 4-Labels & Legend ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Labels & Legend"') + +# -------------------------------------------------------------------------- +# Chart 1: Data labels with position, font, and number format +# +# officecli add charts-area.xlsx "/4-Labels & Legend" --type chart \ +# --prop chartType=area \ +# --prop title="Labeled Area Chart" \ +# --prop series1="Users:3200,3800,4500,5100,5800,6400" \ +# --prop categories=Jul,Aug,Sep,Oct,Nov,Dec \ +# --prop colors=4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=top \ +# --prop labelFont=9:333333:true \ +# --prop dataLabels.numFmt=#,##0 +# +# Features: dataLabels, labelPos (top), labelFont (size:color:bold), +# dataLabels.numFmt +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Labels & Legend" --type chart' + f' --prop chartType=area' + f' --prop title="Labeled Area Chart"' + f' --prop series1="Users:3200,3800,4500,5100,5800,6400"' + f' --prop categories=Jul,Aug,Sep,Oct,Nov,Dec' + f' --prop colors=4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelPos=top' + f' --prop labelFont=9:333333:true' + f' --prop dataLabels.numFmt=#,##0') + +# -------------------------------------------------------------------------- +# Chart 2: Individual label deletion and per-point colors +# +# officecli add charts-area.xlsx "/4-Labels & Legend" --type chart \ +# --prop chartType=area \ +# --prop title="Highlighted Peak Month" \ +# --prop series1="Revenue:180,210,250,310,280,260" \ +# --prop categories=Jul,Aug,Sep,Oct,Nov,Dec \ +# --prop colors=2E75B6 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop dataLabels=true \ +# --prop dataLabel1.delete=true --prop dataLabel2.delete=true \ +# --prop dataLabel5.delete=true --prop dataLabel6.delete=true \ +# --prop point4.color=C00000 \ +# --prop transparency=30 +# +# Features: dataLabel{N}.delete, point{N}.color +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Labels & Legend" --type chart' + f' --prop chartType=area' + f' --prop title="Highlighted Peak Month"' + f' --prop series1="Revenue:180,210,250,310,280,260"' + f' --prop categories=Jul,Aug,Sep,Oct,Nov,Dec' + f' --prop colors=2E75B6' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop dataLabels=true' + f' --prop dataLabel1.delete=true --prop dataLabel2.delete=true' + f' --prop dataLabel5.delete=true --prop dataLabel6.delete=true' + f' --prop point4.color=C00000' + f' --prop transparency=30') + +# -------------------------------------------------------------------------- +# Chart 3: Legend positioning with overlay and font styling +# +# officecli add charts-area.xlsx "/4-Labels & Legend" --type chart \ +# --prop chartType=area \ +# --prop title="Legend Overlay Demo" \ +# --prop series1="Desktop:4200,4800,5100,5600" \ +# --prop series2="Mobile:3100,3500,3800,4200" \ +# --prop series3="Tablet:1200,1400,1500,1700" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=right --prop legendfont=10:1F4E79:Calibri \ +# --prop legend.overlay=true \ +# --prop transparency=35 +# +# Features: legend=right, legendfont, legend.overlay +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Labels & Legend" --type chart' + f' --prop chartType=area' + f' --prop title="Legend Overlay Demo"' + f' --prop series1="Desktop:4200,4800,5100,5600"' + f' --prop series2="Mobile:3100,3500,3800,4200"' + f' --prop series3="Tablet:1200,1400,1500,1700"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=right --prop legendfont=10:1F4E79:Calibri' + f' --prop legend.overlay=true' + f' --prop transparency=35') + +# -------------------------------------------------------------------------- +# Chart 4: Manual layout — plotArea positioning +# +# officecli add charts-area.xlsx "/4-Labels & Legend" --type chart \ +# --prop chartType=area \ +# --prop title="Manual Layout" \ +# --prop series1="Growth:100,130,170,220,280,350" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop colors=70AD47 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop plotArea.x=0.12 --prop plotArea.y=0.18 \ +# --prop plotArea.w=0.82 --prop plotArea.h=0.55 \ +# --prop title.x=0.25 --prop title.y=0.02 \ +# --prop legend.x=0.15 --prop legend.y=0.82 \ +# --prop legend.w=0.7 --prop legend.h=0.12 +# +# Features: plotArea.x/y/w/h, title.x/y, legend.x/y/w/h (manual layout) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Labels & Legend" --type chart' + f' --prop chartType=area' + f' --prop title="Manual Layout"' + f' --prop series1="Growth:100,130,170,220,280,350"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop colors=70AD47' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop plotArea.x=0.12 --prop plotArea.y=0.18' + f' --prop plotArea.w=0.82 --prop plotArea.h=0.55' + f' --prop title.x=0.25 --prop title.y=0.02' + f' --prop legend.x=0.15 --prop legend.y=0.82' + f' --prop legend.w=0.7 --prop legend.h=0.12') + +# ========================================================================== +# Sheet: 5-Advanced +# ========================================================================== +print("\n--- 5-Advanced ---") +cli(f'add "{FILE}" / --type sheet --prop name="5-Advanced"') + +# -------------------------------------------------------------------------- +# Chart 1: Secondary axis (dual scale) +# +# officecli add charts-area.xlsx "/5-Advanced" --type chart \ +# --prop chartType=area \ +# --prop title="Revenue vs Conversion Rate" \ +# --prop series1="Revenue:120,180,250,310,280,340" \ +# --prop series2="Conv %:2.1,2.8,3.2,3.9,3.5,4.1" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop colors=4472C4,C00000 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop secondaryAxis=2 \ +# --prop transparency=30 +# +# Features: secondaryAxis (1-based series index on secondary Y axis) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Advanced" --type chart' + f' --prop chartType=area' + f' --prop title="Revenue vs Conversion Rate"' + f' --prop series1="Revenue:120,180,250,310,280,340"' + f' --prop series2="Conv %:2.1,2.8,3.2,3.9,3.5,4.1"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop colors=4472C4,C00000' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop secondaryAxis=2' + f' --prop transparency=30') + +# -------------------------------------------------------------------------- +# Chart 2: Reference line +# +# officecli add charts-area.xlsx "/5-Advanced" --type chart \ +# --prop chartType=area \ +# --prop title="Sales vs Target" \ +# --prop series1="Sales:85,92,108,115,98,120" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop colors=4472C4 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop referenceLine=100:FF0000:1.5:dash \ +# --prop transparency=25 \ +# --prop areafill=4472C4-BDD7EE:90 +# +# Features: referenceLine (value:color:width:dash) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Advanced" --type chart' + f' --prop chartType=area' + f' --prop title="Sales vs Target"' + f' --prop series1="Sales:85,92,108,115,98,120"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop colors=4472C4' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop referenceLine=100:FF0000:1.5:dash' + f' --prop transparency=25' + f' --prop areafill=4472C4-BDD7EE:90') + +# -------------------------------------------------------------------------- +# Chart 3: Axis min/max, major unit, log scale, display units +# +# officecli add charts-area.xlsx "/5-Advanced" --type chart \ +# --prop chartType=area \ +# --prop title="Axis Scaling Demo" \ +# --prop series1="Visits:3200,3800,4500,5100,5800,6400" \ +# --prop categories=Jul,Aug,Sep,Oct,Nov,Dec \ +# --prop colors=2E75B6 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop axisMin=3000 --prop axisMax=7000 \ +# --prop majorUnit=500 \ +# --prop dispUnits=thousands \ +# --prop axisTitle=Visitors (K) \ +# --prop transparency=30 +# +# Features: axisMin, axisMax, majorUnit, dispUnits (thousands/millions) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Advanced" --type chart' + f' --prop chartType=area' + f' --prop title="Axis Scaling Demo"' + f' --prop series1="Visits:3200,3800,4500,5100,5800,6400"' + f' --prop categories=Jul,Aug,Sep,Oct,Nov,Dec' + f' --prop colors=2E75B6' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop axisMin=3000 --prop axisMax=7000' + f' --prop majorUnit=500' + f' --prop dispUnits=thousands' + f' --prop "axisTitle=Visitors (K)"' + f' --prop transparency=30') + +# -------------------------------------------------------------------------- +# Chart 4: Color rule, title glow, series shadow +# +# officecli add charts-area.xlsx "/5-Advanced" --type chart \ +# --prop chartType=area \ +# --prop title="Performance Threshold" \ +# --prop series1="Score:45,62,38,71,55,80" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colorRule=50:C00000:70AD47 \ +# --prop referenceLine=50:888888:1:solid \ +# --prop title.glow=4472C4-8-60 \ +# --prop series.shadow=000000-3-315-1-30 \ +# --prop transparency=20 +# +# Features: colorRule (threshold:belowColor:aboveColor), title.glow +# (color-radius-opacity), series.shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Advanced" --type chart' + f' --prop chartType=area' + f' --prop title="Performance Threshold"' + f' --prop series1="Score:45,62,38,71,55,80"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop colorRule=50:C00000:70AD47' + f' --prop referenceLine=50:888888:1:solid' + f' --prop title.glow=4472C4-8-60' + f' --prop series.shadow=000000-3-315-1-30' + f' --prop transparency=20') + +print(f"\nDone! Generated: {FILE}") +print(" 6 sheets (Sheet1 data + 5 chart sheets, 20 charts total)") diff --git a/examples/excel/charts/charts-area.xlsx b/examples/excel/charts/charts-area.xlsx new file mode 100644 index 000000000..f45ecf4ca Binary files /dev/null and b/examples/excel/charts/charts-area.xlsx differ diff --git a/examples/excel/charts/charts-bar.md b/examples/excel/charts/charts-bar.md new file mode 100644 index 000000000..2a977aa63 --- /dev/null +++ b/examples/excel/charts/charts-bar.md @@ -0,0 +1,274 @@ +# Bar (Horizontal) Charts Showcase + +This demo consists of three files that work together: + +- **charts-bar.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-bar.xlsx** — The generated workbook with 7 sheets (1 data + 6 chart sheets, 24 charts total). +- **charts-bar.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-bar.py +# → charts-bar.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Bar Fundamentals + +Four basic horizontal bar charts covering data input variants, colors, stacking, and shorthand syntax. + +```bash +# Basic bar from cell range with axis titles and gridlines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop dataRange=Sheet1!A1:B9 \ + --prop catTitle=Department --prop axisTitle=Score \ + --prop axisfont=9:333333:Arial \ + --prop gridlines=D9D9D9:0.5:dot + +# Inline series with custom colors and data labels +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop series1="Satisfaction:85,72,91,68,78" \ + --prop colors=4472C4,ED7D31,70AD47,FFC000,5B9BD5 \ + --prop gapwidth=80 --prop dataLabels=outsideEnd + +# Stacked bar with series outline +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=barStacked \ + --prop series1="Q1:30,18,25,12" --prop series2="Q2:35,20,28,14" \ + --prop overlap=0 --prop series.outline=FFFFFF-0.5 + +# data= shorthand with legend at bottom +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop 'data=Technical:45,38,52;Soft Skills:20,28,18;Compliance:12,15,10' \ + --prop legend=bottom +``` + +**Features:** `bar`, `barStacked`, `dataRange`, `catTitle`, `axisTitle`, `axisfont`, `gridlines`, `colors`, `gapwidth`, `dataLabels=outsideEnd`, `overlap`, `series.outline`, `data=` shorthand, `legend=bottom` + +### Sheet: 2-Bar Variants + +Four bar chart type variants: stacked, 100% stacked, 3D, and 3D cylinder. + +```bash +# Stacked bar with tight gap +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=barStacked \ + --prop gapwidth=50 + +# 100% stacked with percentage axis and reference line +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=barPercentStacked \ + --prop axisNumFmt=0% \ + --prop referenceLine=0.5:FF0000:Target:dash + +# 3D bar with perspective +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar3d \ + --prop view3d=10,30,20 --prop style=3 + +# 3D bar with cylinder shape +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar3d \ + --prop shape=cylinder --prop gapwidth=60 +``` + +**Features:** `barStacked`, `barPercentStacked`, `bar3d`, `gapwidth`, `axisNumFmt=0%`, `referenceLine` (with label and dash), `view3d`, `style`, `shape=cylinder` + +### Sheet: 3-Bar Styling + +Four charts demonstrating visual styling: title formatting, shadows, gradients, and background fills. + +```bash +# Title font, size, color, bold +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop title.font=Georgia --prop title.size=16 \ + --prop title.color=1F4E79 --prop title.bold=true + +# Series shadow and outline +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop series.shadow=000000-4-315-2-30 \ + --prop series.outline=1F4E79-1 + +# Per-bar gradient fills (angle=0 for horizontal) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop 'gradients=1F4E79-5B9BD5:0;C55A11-F4B183:0;...' \ + --prop labelFont=9:333333:true + +# Plot/chart fill with transparency and rounded corners +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop plotFill=F0F4F8-D6E4F0:90 --prop chartFill=FFFFFF \ + --prop transparency=20 --prop roundedCorners=true +``` + +**Features:** `title.font/size/color/bold`, `series.shadow`, `series.outline`, `gradients` (per-bar), `labelFont`, `plotFill` gradient, `chartFill`, `transparency`, `roundedCorners` + +### Sheet: 4-Axis & Labels + +Four charts exploring axis configuration and data label customization. + +```bash +# Custom axis scale with gridlines styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop axisMin=50 --prop axisMax=250 --prop majorUnit=50 \ + --prop gridlines=D0D0D0:0.5:solid \ + --prop minorGridlines=EEEEEE:0.3:dot \ + --prop axisLine=C00000:1.5:solid --prop catAxisLine=2E75B6:1.5:solid + +# Log scale, reversed axis, display units +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop logBase=10 --prop axisReverse=true \ + --prop dispUnits=thousands + +# Data labels with font, number format, separator +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop dataLabels=true --prop labelPos=outsideEnd \ + --prop labelFont=10:1F4E79:true \ + --prop dataLabels.numFmt=#,##0 --prop "dataLabels.separator=: " + +# Per-point label delete/text and per-point color (highlight winner) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop dataLabel1.delete=true --prop dataLabel4.text="Winner!" \ + --prop point4.color=C00000 --prop point2.color=2E75B6 +``` + +**Features:** `axisMin`, `axisMax`, `majorUnit`, `gridlines`, `minorGridlines`, `axisLine`, `catAxisLine`, `logBase`, `axisReverse`, `dispUnits`, `dataLabels`, `labelPos`, `labelFont`, `dataLabels.numFmt`, `dataLabels.separator`, `dataLabel{N}.delete`, `dataLabel{N}.text`, `point{N}.color` + +### Sheet: 5-Legend & Layout + +Four charts covering legend configuration, manual layout, and dual-axis support. + +```bash +# Legend on right side +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop legend=right + +# Legend font styling with overlay +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop legend=top --prop legend.overlay=true \ + --prop legendfont=10:1F4E79:Calibri + +# Manual layout: plotArea, title, and legend positioning +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop plotArea.x=0.25 --prop plotArea.y=0.15 \ + --prop plotArea.w=0.70 --prop plotArea.h=0.60 \ + --prop title.x=0.20 --prop title.y=0.02 \ + --prop legend.x=0.25 --prop legend.y=0.82 + +# Secondary axis with chart/plot area borders +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop secondaryAxis=2 \ + --prop chartArea.border=D0D0D0:1:solid \ + --prop plotArea.border=E0E0E0:0.5:dot +``` + +**Features:** `legend=right/top/bottom`, `legend.overlay`, `legendfont`, `plotArea.x/y/w/h`, `title.x/y`, `legend.x/y/w/h`, `secondaryAxis`, `chartArea.border`, `plotArea.border` + +### Sheet: 6-Advanced + +Four charts with advanced features: reference lines, conditional coloring, effects, and data tables. + +```bash +# Reference line with label +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop referenceLine=79:FF0000:Average:dash + +# Conditional coloring (profit/loss) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop colorRule=0:C00000:70AD47 \ + --prop referenceLine=0:888888:1:solid + +# Title glow, title shadow, series shadow +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop title.glow=4472C4-8-60 \ + --prop title.shadow=000000-3-315-2-40 \ + --prop series.shadow=000000-3-315-1-30 + +# Error bars and data table +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop errBars=percent:10 --prop dataTable=true \ + --prop legend=none +``` + +**Features:** `referenceLine` (with label), `colorRule` (threshold coloring), `title.glow`, `title.shadow`, `series.shadow`, `errBars=percent:10`, `dataTable=true` + +## Feature Coverage + +| Feature | Sheet | +|---|---| +| `bar` (basic horizontal) | 1, 3, 4, 5, 6 | +| `barStacked` | 1, 2 | +| `barPercentStacked` | 2 | +| `bar3d` | 2 | +| `bar3d shape=cylinder` | 2 | +| `dataRange` (cell reference) | 1, 3, 5, 6 | +| `data=` shorthand | 1 | +| `series1=Name:values` | 1, 2, 3, 4, 5, 6 | +| `colors` | 1, 2, 3, 4, 5, 6 | +| `gapwidth` | 1, 2, 4, 6 | +| `overlap` | 1 | +| `dataLabels` / `labelPos` | 1, 3, 4, 6 | +| `labelFont` | 3, 4, 6 | +| `dataLabels.numFmt` | 4 | +| `dataLabels.separator` | 4 | +| `dataLabel{N}.delete/text` | 4 | +| `point{N}.color` | 4 | +| `catTitle` / `axisTitle` | 1 | +| `axisfont` | 1 | +| `axisMin/Max` / `majorUnit` | 4 | +| `gridlines` / `minorGridlines` | 1, 4, 6 | +| `axisLine` / `catAxisLine` | 4 | +| `logBase` | 4 | +| `axisReverse` | 4 | +| `dispUnits` | 4 | +| `axisNumFmt` | 2 | +| `legend` positions | 1, 2, 5, 6 | +| `legendfont` | 5 | +| `legend.overlay` | 5 | +| `title.font/size/color/bold` | 3 | +| `title.glow` / `title.shadow` | 6 | +| `series.shadow` | 3, 6 | +| `series.outline` | 1, 3 | +| `gradients` | 3 | +| `plotFill` / `chartFill` | 3, 6 | +| `transparency` | 3 | +| `roundedCorners` | 3 | +| `referenceLine` | 2, 6 | +| `colorRule` | 6 | +| `secondaryAxis` | 5 | +| `chartArea.border` / `plotArea.border` | 5 | +| `plotArea.x/y/w/h` | 5 | +| `title.x/y` | 5 | +| `legend.x/y/w/h` | 5 | +| `view3d` / `style` | 2 | +| `shape=cylinder` | 2 | +| `errBars` | 6 | +| `dataTable` | 6 | + +## Inspect the Generated File + +```bash +officecli query charts-bar.xlsx chart +officecli get charts-bar.xlsx "/1-Bar Fundamentals/chart[1]" +``` diff --git a/examples/excel/charts/charts-bar.py b/examples/excel/charts/charts-bar.py new file mode 100644 index 000000000..2c7585304 --- /dev/null +++ b/examples/excel/charts/charts-bar.py @@ -0,0 +1,788 @@ +#!/usr/bin/env python3 +""" +Bar (Horizontal) Charts Showcase — bar, barStacked, barPercentStacked, and bar3d with all variations. + +Generates: charts-bar.xlsx + +Every horizontal bar chart feature officecli supports is demonstrated at least once: +gap width, overlap, data labels, axis scaling, gridlines, legend positioning, +reference lines, secondary axis, error bars, gradients, transparency, shadows, +manual layout, data table, 3D rotation, and conditional coloring. + +6 sheets, 24 charts total. + + 1-Bar Fundamentals 4 charts — data input variants, colors, stacked, data shorthand + 2-Bar Variants 4 charts — barStacked, barPercentStacked, bar3d, cylinder + 3-Bar Styling 4 charts — title styling, shadow/outline, gradients, plot/chart fill + 4-Axis & Labels 4 charts — axis scale, log/reverse/dispUnits, label styling, per-point + 5-Legend & Layout 4 charts — legend positions, overlay, manual layout, secondary axis + 6-Advanced 4 charts — reference line, colorRule, glow/shadow, errBars/dataTable + +Usage: + python3 charts-bar.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-bar.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Source data — shared across all charts +# ========================================================================== +print("\n--- Populating source data ---") + +data_cmds = [] +for j, h in enumerate(["Department", "Q1", "Q2", "Q3", "Q4"]): + data_cmds.append({"command": "set", "path": f"/Sheet1/{'ABCDE'[j]}1", "props": {"text": h, "bold": "true"}}) + +depts = ["Engineering", "Marketing", "Sales", "Support", "Finance", "HR", "Legal", "Operations"] +q1 = [185, 120, 210, 95, 78, 62, 55, 140] +q2 = [195, 135, 225, 105, 82, 68, 58, 152] +q3 = [210, 142, 240, 112, 88, 72, 62, 165] +q4 = [228, 158, 260, 118, 92, 78, 68, 178] + +for i in range(8): + r = i + 2 + for j, val in enumerate([depts[i], q1[i], q2[i], q3[i], q4[i]]): + data_cmds.append({"command": "set", "path": f"/Sheet1/{'ABCDE'[j]}{r}", "props": {"text": str(val)}}) + +cli(f'batch "{FILE}" --force --commands \'{json.dumps(data_cmds)}\'') + +# ========================================================================== +# Sheet: 1-Bar Fundamentals +# ========================================================================== +print("\n--- 1-Bar Fundamentals ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Bar Fundamentals"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic bar chart with dataRange, axis titles, and gridlines +# +# officecli add charts-bar.xlsx "/1-Bar Fundamentals" --type chart \ +# --prop chartType=bar \ +# --prop title="Department Performance — Q1" \ +# --prop dataRange=Sheet1!A1:B9 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop catTitle=Department --prop axisTitle=Score \ +# --prop axisfont=9:333333:Arial \ +# --prop gridlines=D9D9D9:0.5:dot +# +# Features: chartType=bar, dataRange, catTitle, axisTitle, axisfont, gridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Bar Fundamentals" --type chart' + f' --prop chartType=bar' + f' --prop title="Department Performance — Q1"' + f' --prop dataRange=Sheet1!A1:B9' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop catTitle=Department --prop axisTitle=Score' + f' --prop axisfont=9:333333:Arial' + f' --prop gridlines=D9D9D9:0.5:dot') + +# -------------------------------------------------------------------------- +# Chart 2: Inline series with custom colors, gap width, and data labels +# +# officecli add charts-bar.xlsx "/1-Bar Fundamentals" --type chart \ +# --prop chartType=bar \ +# --prop title="Survey Results" \ +# --prop series1="Satisfaction:85,72,91,68,78" \ +# --prop categories=Product,Service,Delivery,Price,Overall \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000,5B9BD5 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop gapwidth=80 \ +# --prop dataLabels=outsideEnd +# +# Features: inline series, colors per category, gapwidth, dataLabels=outsideEnd +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Bar Fundamentals" --type chart' + f' --prop chartType=bar' + f' --prop title="Survey Results"' + f' --prop series1=Satisfaction:85,72,91,68,78' + f' --prop categories=Product,Service,Delivery,Price,Overall' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000,5B9BD5' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop gapwidth=80' + f' --prop dataLabels=outsideEnd') + +# -------------------------------------------------------------------------- +# Chart 3: Stacked bar with overlap and series outline +# +# officecli add charts-bar.xlsx "/1-Bar Fundamentals" --type chart \ +# --prop chartType=barStacked \ +# --prop title="Quarterly Headcount by Dept" \ +# --prop series1="Q1:30,18,25,12" \ +# --prop series2="Q2:35,20,28,14" \ +# --prop series3="Q3:38,22,30,16" \ +# --prop categories=Engineering,Marketing,Sales,Support \ +# --prop colors=2E75B6,70AD47,FFC000 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop overlap=0 \ +# --prop series.outline=FFFFFF-0.5 +# +# Features: barStacked, overlap=0, series.outline (white separator) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Bar Fundamentals" --type chart' + f' --prop chartType=barStacked' + f' --prop title="Quarterly Headcount by Dept"' + f' --prop series1=Q1:30,18,25,12' + f' --prop series2=Q2:35,20,28,14' + f' --prop series3=Q3:38,22,30,16' + f' --prop categories=Engineering,Marketing,Sales,Support' + f' --prop colors=2E75B6,70AD47,FFC000' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop overlap=0' + f' --prop series.outline=FFFFFF-0.5') + +# -------------------------------------------------------------------------- +# Chart 4: data= shorthand with legend=bottom +# +# officecli add charts-bar.xlsx "/1-Bar Fundamentals" --type chart \ +# --prop chartType=bar \ +# --prop title="Training Hours by Team" \ +# --prop 'data=Technical:45,38,52;Soft Skills:20,28,18;Compliance:12,15,10' \ +# --prop categories=Engineering,Sales,Support \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=bottom +# +# Features: data= shorthand (inline multi-series), legend=bottom +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Bar Fundamentals" --type chart' + f' --prop chartType=bar' + f' --prop title="Training Hours by Team"' + f' --prop "data=Technical:45,38,52;Soft Skills:20,28,18;Compliance:12,15,10"' + f' --prop categories=Engineering,Sales,Support' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 2-Bar Variants +# ========================================================================== +print("\n--- 2-Bar Variants ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Bar Variants"') + +# -------------------------------------------------------------------------- +# Chart 1: barStacked with tight gap width +# +# officecli add charts-bar.xlsx "/2-Bar Variants" --type chart \ +# --prop chartType=barStacked \ +# --prop title="Budget Allocation" \ +# --prop series1="Salaries:120,80,95,60" \ +# --prop series2="Operations:45,35,40,25" \ +# --prop series3="Marketing:30,50,20,15" \ +# --prop categories=Engineering,Sales,Support,HR \ +# --prop colors=1F4E79,2E75B6,9DC3E6 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop gapwidth=50 \ +# --prop legend=bottom +# +# Features: barStacked, gapwidth=50 (tight bars) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bar Variants" --type chart' + f' --prop chartType=barStacked' + f' --prop title="Budget Allocation"' + f' --prop series1=Salaries:120,80,95,60' + f' --prop series2=Operations:45,35,40,25' + f' --prop series3=Marketing:30,50,20,15' + f' --prop categories=Engineering,Sales,Support,HR' + f' --prop colors=1F4E79,2E75B6,9DC3E6' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop gapwidth=50' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: barPercentStacked with axis number format and reference line +# +# officecli add charts-bar.xlsx "/2-Bar Variants" --type chart \ +# --prop chartType=barPercentStacked \ +# --prop title="Task Completion Ratio" \ +# --prop series1="Done:75,60,90,45,80" \ +# --prop series2="In Progress:15,25,5,30,12" \ +# --prop series3="Blocked:10,15,5,25,8" \ +# --prop categories=Backend,Frontend,QA,Design,DevOps \ +# --prop colors=70AD47,FFC000,C00000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop axisNumFmt=0% \ +# --prop referenceLine=0.5:FF0000:Target:dash \ +# --prop legend=bottom +# +# Features: barPercentStacked, axisNumFmt=0%, referenceLine with label and dash +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bar Variants" --type chart' + f' --prop chartType=barPercentStacked' + f' --prop title="Task Completion Ratio"' + f' --prop series1=Done:75,60,90,45,80' + f' --prop series2="In Progress:15,25,5,30,12"' + f' --prop series3=Blocked:10,15,5,25,8' + f' --prop categories=Backend,Frontend,QA,Design,DevOps' + f' --prop colors=70AD47,FFC000,C00000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop axisNumFmt=0%' + f' --prop referenceLine=0.5:FF0000:Target:dash' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: bar3d with perspective and style +# +# officecli add charts-bar.xlsx "/2-Bar Variants" --type chart \ +# --prop chartType=bar3d \ +# --prop title="3D Revenue by Region" \ +# --prop series1="Revenue:340,280,310,195" \ +# --prop categories=North,South,East,West \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=10,30,20 \ +# --prop style=3 \ +# --prop legend=right +# +# Features: bar3d, view3d (rotX,rotY,perspective), style=3 +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bar Variants" --type chart' + f' --prop chartType=bar3d' + f' --prop title="3D Revenue by Region"' + f' --prop series1=Revenue:340,280,310,195' + f' --prop categories=North,South,East,West' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=10,30,20' + f' --prop style=3' + f' --prop legend=right') + +# -------------------------------------------------------------------------- +# Chart 4: bar3d with cylinder shape +# +# officecli add charts-bar.xlsx "/2-Bar Variants" --type chart \ +# --prop chartType=bar3d \ +# --prop title="Cylinder — Project Milestones" \ +# --prop series1="Completed:8,12,6,10,15" \ +# --prop series2="Remaining:4,3,6,5,2" \ +# --prop categories=Alpha,Beta,Gamma,Delta,Epsilon \ +# --prop colors=2E75B6,BDD7EE \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop shape=cylinder \ +# --prop gapwidth=60 \ +# --prop legend=bottom +# +# Features: bar3d shape=cylinder, multi-series 3D bars +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bar Variants" --type chart' + f' --prop chartType=bar3d' + f' --prop title="Cylinder — Project Milestones"' + f' --prop series1=Completed:8,12,6,10,15' + f' --prop series2=Remaining:4,3,6,5,2' + f' --prop categories=Alpha,Beta,Gamma,Delta,Epsilon' + f' --prop colors=2E75B6,BDD7EE' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop shape=cylinder' + f' --prop gapwidth=60' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 3-Bar Styling +# ========================================================================== +print("\n--- 3-Bar Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Bar Styling"') + +# -------------------------------------------------------------------------- +# Chart 1: Title styling (font, size, color, bold) +# +# officecli add charts-bar.xlsx "/3-Bar Styling" --type chart \ +# --prop chartType=bar \ +# --prop title="Styled Title Demo" \ +# --prop series1="Score:88,76,92,65,84" \ +# --prop categories=Dept A,Dept B,Dept C,Dept D,Dept E \ +# --prop colors=4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop title.font=Georgia --prop title.size=16 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop gapwidth=100 +# +# Features: title.font, title.size, title.color, title.bold +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Bar Styling" --type chart' + f' --prop chartType=bar' + f' --prop title="Styled Title Demo"' + f' --prop series1=Score:88,76,92,65,84' + f' --prop categories=Dept A,Dept B,Dept C,Dept D,Dept E' + f' --prop colors=4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop title.font=Georgia --prop title.size=16' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop gapwidth=100') + +# -------------------------------------------------------------------------- +# Chart 2: Series shadow and outline effects +# +# officecli add charts-bar.xlsx "/3-Bar Styling" --type chart \ +# --prop chartType=bar \ +# --prop title="Shadow & Outline" \ +# --prop series1="2024:165,142,180,128" \ +# --prop series2="2025:185,158,195,140" \ +# --prop categories=Engineering,Marketing,Sales,Support \ +# --prop colors=2E75B6,ED7D31 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop series.shadow=000000-4-315-2-30 \ +# --prop series.outline=1F4E79-1 \ +# --prop legend=bottom +# +# Features: series.shadow (color-blur-angle-dist-opacity), series.outline +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Bar Styling" --type chart' + f' --prop chartType=bar' + f' --prop title="Shadow & Outline"' + f' --prop series1=2024:165,142,180,128' + f' --prop series2=2025:185,158,195,140' + f' --prop categories=Engineering,Marketing,Sales,Support' + f' --prop colors=2E75B6,ED7D31' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop series.shadow=000000-4-315-2-30' + f' --prop series.outline=1F4E79-1' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Per-series gradients +# +# officecli add charts-bar.xlsx "/3-Bar Styling" --type chart \ +# --prop chartType=bar \ +# --prop title="Gradient Bars" \ +# --prop series1="Revenue:320,275,410,190,245" \ +# --prop categories=North,South,East,West,Central \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop 'gradients=1F4E79-5B9BD5:0;C55A11-F4B183:0;548235-A9D18E:0;7F6000-FFD966:0;843C0B-DDA15E:0' \ +# --prop dataLabels=outsideEnd \ +# --prop labelFont=9:333333:true +# +# Features: gradients (per-bar gradient fills, angle=0 for horizontal), +# labelFont (size:color:bold) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Bar Styling" --type chart' + f' --prop chartType=bar' + f' --prop title="Gradient Bars"' + f' --prop series1=Revenue:320,275,410,190,245' + f' --prop categories=North,South,East,West,Central' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop "gradients=1F4E79-5B9BD5:0;C55A11-F4B183:0;548235-A9D18E:0;7F6000-FFD966:0;843C0B-DDA15E:0"' + f' --prop dataLabels=outsideEnd' + f' --prop labelFont=9:333333:true') + +# -------------------------------------------------------------------------- +# Chart 4: Plot fill gradient, chart fill, transparency, rounded corners +# +# officecli add charts-bar.xlsx "/3-Bar Styling" --type chart \ +# --prop chartType=bar \ +# --prop title="Styled Background" \ +# --prop dataRange=Sheet1!A1:C9 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=5B9BD5,ED7D31 \ +# --prop plotFill=F0F4F8-D6E4F0:90 \ +# --prop chartFill=FFFFFF \ +# --prop transparency=20 \ +# --prop roundedCorners=true \ +# --prop legend=right +# +# Features: plotFill gradient, chartFill, transparency, roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Bar Styling" --type chart' + f' --prop chartType=bar' + f' --prop title="Styled Background"' + f' --prop dataRange=Sheet1!A1:C9' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=5B9BD5,ED7D31' + f' --prop plotFill=F0F4F8-D6E4F0:90' + f' --prop chartFill=FFFFFF' + f' --prop transparency=20' + f' --prop roundedCorners=true' + f' --prop legend=right') + +# ========================================================================== +# Sheet: 4-Axis & Labels +# ========================================================================== +print("\n--- 4-Axis & Labels ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Axis & Labels"') + +# -------------------------------------------------------------------------- +# Chart 1: Custom axis min/max, majorUnit, and gridlines styling +# +# officecli add charts-bar.xlsx "/4-Axis & Labels" --type chart \ +# --prop chartType=bar \ +# --prop title="Axis Scale (50–250)" \ +# --prop dataRange=Sheet1!A1:B9 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop axisMin=50 --prop axisMax=250 --prop majorUnit=50 \ +# --prop gridlines=D0D0D0:0.5:solid \ +# --prop minorGridlines=EEEEEE:0.3:dot \ +# --prop axisLine=C00000:1.5:solid \ +# --prop catAxisLine=2E75B6:1.5:solid +# +# Features: axisMin, axisMax, majorUnit, gridlines styling, +# minorGridlines, axisLine, catAxisLine +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Labels" --type chart' + f' --prop chartType=bar' + f' --prop title="Axis Scale (50–250)"' + f' --prop dataRange=Sheet1!A1:B9' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop axisMin=50 --prop axisMax=250 --prop majorUnit=50' + f' --prop gridlines=D0D0D0:0.5:solid' + f' --prop minorGridlines=EEEEEE:0.3:dot' + f' --prop axisLine=C00000:1.5:solid' + f' --prop catAxisLine=2E75B6:1.5:solid') + +# -------------------------------------------------------------------------- +# Chart 2: Log scale, axis reverse, and display units +# +# officecli add charts-bar.xlsx "/4-Axis & Labels" --type chart \ +# --prop chartType=bar \ +# --prop title="Log Scale & Reverse" \ +# --prop series1="Users:10,100,1000,5000,25000,100000" \ +# --prop categories=Tier 1,Tier 2,Tier 3,Tier 4,Tier 5,Tier 6 \ +# --prop colors=2E75B6 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop logBase=10 \ +# --prop axisReverse=true \ +# --prop dispUnits=thousands \ +# --prop gridlines=E0E0E0:0.5:dash +# +# Features: logBase=10, axisReverse=true, dispUnits=thousands +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Labels" --type chart' + f' --prop chartType=bar' + f' --prop title="Log Scale & Reverse"' + f' --prop "series1=Users:10,100,1000,5000,25000,100000"' + f' --prop "categories=Tier 1,Tier 2,Tier 3,Tier 4,Tier 5,Tier 6"' + f' --prop colors=2E75B6' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop logBase=10' + f' --prop axisReverse=true' + f' --prop dispUnits=thousands' + f' --prop gridlines=E0E0E0:0.5:dash') + +# -------------------------------------------------------------------------- +# Chart 3: Data labels with labelFont, numFmt, separator +# +# officecli add charts-bar.xlsx "/4-Axis & Labels" --type chart \ +# --prop chartType=bar \ +# --prop title="Labeled Metrics" \ +# --prop series1="FY2025:148,92,215,178,125" \ +# --prop categories=Revenue,Costs,Gross,EBITDA,Net Income \ +# --prop colors=4472C4 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=outsideEnd \ +# --prop labelFont=10:1F4E79:true \ +# --prop dataLabels.numFmt=#,##0 \ +# --prop "dataLabels.separator=: " +# +# Features: dataLabels, labelFont, dataLabels.numFmt, dataLabels.separator +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Labels" --type chart' + f' --prop chartType=bar' + f' --prop title="Labeled Metrics"' + f' --prop series1=FY2025:148,92,215,178,125' + f' --prop categories=Revenue,Costs,Gross,EBITDA,Net Income' + f' --prop colors=4472C4' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop dataLabels=outsideEnd' + f' --prop labelFont=10:1F4E79:true' + f' --prop dataLabels.numFmt=#,##0' + f' --prop "dataLabels.separator=: "') + +# -------------------------------------------------------------------------- +# Chart 4: Per-point label delete/text and per-point color +# +# officecli add charts-bar.xlsx "/4-Axis & Labels" --type chart \ +# --prop chartType=bar \ +# --prop title="Highlight Winner" \ +# --prop series1="Score:72,85,68,95,78" \ +# --prop categories=Team A,Team B,Team C,Team D,Team E \ +# --prop colors=9DC3E6 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=outsideEnd \ +# --prop dataLabel1.delete=true --prop dataLabel3.delete=true \ +# --prop dataLabel5.delete=true \ +# --prop dataLabel4.text="Winner!" \ +# --prop point4.color=C00000 \ +# --prop point2.color=2E75B6 \ +# --prop gapwidth=70 +# +# Features: dataLabel{N}.delete, dataLabel{N}.text, point{N}.color +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Labels" --type chart' + f' --prop chartType=bar' + f' --prop title="Highlight Winner"' + f' --prop series1=Score:72,85,68,95,78' + f' --prop categories=Team A,Team B,Team C,Team D,Team E' + f' --prop colors=9DC3E6' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelPos=outsideEnd' + f' --prop dataLabel1.delete=true --prop dataLabel3.delete=true' + f' --prop dataLabel5.delete=true' + f' --prop dataLabel4.text="Winner!"' + f' --prop point4.color=C00000' + f' --prop point2.color=2E75B6' + f' --prop gapwidth=70') + +# ========================================================================== +# Sheet: 5-Legend & Layout +# ========================================================================== +print("\n--- 5-Legend & Layout ---") +cli(f'add "{FILE}" / --type sheet --prop name="5-Legend & Layout"') + +# -------------------------------------------------------------------------- +# Chart 1: Legend positions (right and bottom) +# +# officecli add charts-bar.xlsx "/5-Legend & Layout" --type chart \ +# --prop chartType=bar \ +# --prop title="Legend: Right" \ +# --prop dataRange=Sheet1!A1:E9 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop legend=right +# +# Features: legend=right (4-series bar with legend on right) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Legend & Layout" --type chart' + f' --prop chartType=bar' + f' --prop title="Legend: Right"' + f' --prop dataRange=Sheet1!A1:E9' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop legend=right') + +# -------------------------------------------------------------------------- +# Chart 2: Legend font styling and overlay +# +# officecli add charts-bar.xlsx "/5-Legend & Layout" --type chart \ +# --prop chartType=bar \ +# --prop title="Legend: Font & Overlay" \ +# --prop dataRange=Sheet1!A1:E9 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=1F4E79,2E75B6,5B9BD5,9DC3E6 \ +# --prop legend=top \ +# --prop legend.overlay=true \ +# --prop legendfont=10:1F4E79:Calibri +# +# Features: legendfont (size:color:fontname), legend.overlay=true +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Legend & Layout" --type chart' + f' --prop chartType=bar' + f' --prop title="Legend: Font & Overlay"' + f' --prop dataRange=Sheet1!A1:E9' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=1F4E79,2E75B6,5B9BD5,9DC3E6' + f' --prop legend=top' + f' --prop legend.overlay=true' + f' --prop legendfont=10:1F4E79:Calibri') + +# -------------------------------------------------------------------------- +# Chart 3: Manual layout — plotArea.x/y/w/h, title.x/y +# +# officecli add charts-bar.xlsx "/5-Legend & Layout" --type chart \ +# --prop chartType=bar \ +# --prop title="Manual Layout" \ +# --prop dataRange=Sheet1!A1:C9 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=2E75B6,70AD47 \ +# --prop plotArea.x=0.25 --prop plotArea.y=0.15 \ +# --prop plotArea.w=0.70 --prop plotArea.h=0.60 \ +# --prop title.x=0.20 --prop title.y=0.02 \ +# --prop legend.x=0.25 --prop legend.y=0.82 \ +# --prop legend.w=0.50 --prop legend.h=0.10 \ +# --prop title.font=Arial --prop title.size=13 \ +# --prop title.bold=true +# +# Features: plotArea.x/y/w/h, title.x/y, legend.x/y/w/h (manual layout) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Legend & Layout" --type chart' + f' --prop chartType=bar' + f' --prop title="Manual Layout"' + f' --prop dataRange=Sheet1!A1:C9' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=2E75B6,70AD47' + f' --prop plotArea.x=0.25 --prop plotArea.y=0.15' + f' --prop plotArea.w=0.70 --prop plotArea.h=0.60' + f' --prop title.x=0.20 --prop title.y=0.02' + f' --prop legend.x=0.25 --prop legend.y=0.82' + f' --prop legend.w=0.50 --prop legend.h=0.10' + f' --prop title.font=Arial --prop title.size=13' + f' --prop title.bold=true') + +# -------------------------------------------------------------------------- +# Chart 4: Secondary axis with chart/plot area borders +# +# officecli add charts-bar.xlsx "/5-Legend & Layout" --type chart \ +# --prop chartType=bar \ +# --prop title="Dual Axis: Revenue vs Margin" \ +# --prop series1="Revenue:340,280,410,195,310" \ +# --prop series2="Margin %:22,18,28,15,25" \ +# --prop categories=North,South,East,West,Central \ +# --prop colors=2E75B6,C00000 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop secondaryAxis=2 \ +# --prop chartArea.border=D0D0D0:1:solid \ +# --prop plotArea.border=E0E0E0:0.5:dot \ +# --prop legend=bottom +# +# Features: secondaryAxis=2, chartArea.border, plotArea.border +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Legend & Layout" --type chart' + f' --prop chartType=bar' + f' --prop title="Dual Axis: Revenue vs Margin"' + f' --prop "series1=Revenue:340,280,410,195,310"' + f' --prop "series2=Margin %:22,18,28,15,25"' + f' --prop categories=North,South,East,West,Central' + f' --prop colors=2E75B6,C00000' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop secondaryAxis=2' + f' --prop chartArea.border=D0D0D0:1:solid' + f' --prop plotArea.border=E0E0E0:0.5:dot' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 6-Advanced +# ========================================================================== +print("\n--- 6-Advanced ---") +cli(f'add "{FILE}" / --type sheet --prop name="6-Advanced"') + +# -------------------------------------------------------------------------- +# Chart 1: Reference line with label +# +# officecli add charts-bar.xlsx "/6-Advanced" --type chart \ +# --prop chartType=bar \ +# --prop title="vs Company Average" \ +# --prop series1="Score:82,74,91,68,87,72" \ +# --prop categories=Engineering,Marketing,Sales,Support,Finance,HR \ +# --prop colors=4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop referenceLine=79:FF0000:Average:dash \ +# --prop gapwidth=80 \ +# --prop gridlines=E0E0E0:0.5:solid +# +# Features: referenceLine (value:color:label:dash style) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Advanced" --type chart' + f' --prop chartType=bar' + f' --prop title="vs Company Average"' + f' --prop series1=Score:82,74,91,68,87,72' + f' --prop categories=Engineering,Marketing,Sales,Support,Finance,HR' + f' --prop colors=4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop referenceLine=79:FF0000:Average:dash' + f' --prop gapwidth=80' + f' --prop gridlines=E0E0E0:0.5:solid') + +# -------------------------------------------------------------------------- +# Chart 2: Conditional coloring (colorRule) +# +# officecli add charts-bar.xlsx "/6-Advanced" --type chart \ +# --prop chartType=bar \ +# --prop title="Profit/Loss by Division" \ +# --prop series1="P&L:120,85,-45,160,-80,95,-20,140" \ +# --prop categories=Div A,Div B,Div C,Div D,Div E,Div F,Div G,Div H \ +# --prop colors=2E75B6 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colorRule=0:C00000:70AD47 \ +# --prop referenceLine=0:888888:1:solid \ +# --prop dataLabels=outsideEnd \ +# --prop labelFont=9:333333:false +# +# Features: colorRule (threshold:belowColor:aboveColor), +# referenceLine=0 (zero baseline) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Advanced" --type chart' + f' --prop chartType=bar' + f' --prop title="Profit/Loss by Division"' + f' --prop "series1=P&L:120,85,-45,160,-80,95,-20,140"' + f' --prop categories=Div A,Div B,Div C,Div D,Div E,Div F,Div G,Div H' + f' --prop colors=2E75B6' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop colorRule=0:C00000:70AD47' + f' --prop referenceLine=0:888888:1:solid' + f' --prop dataLabels=outsideEnd' + f' --prop labelFont=9:333333:false') + +# -------------------------------------------------------------------------- +# Chart 3: Title glow, title shadow, series shadow +# +# officecli add charts-bar.xlsx "/6-Advanced" --type chart \ +# --prop chartType=bar \ +# --prop title="Glow & Shadow Effects" \ +# --prop series1="East:185,195,210,228" \ +# --prop series2="West:140,152,165,178" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop colors=4472C4,ED7D31 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop title.glow=4472C4-8-60 \ +# --prop title.shadow=000000-3-315-2-40 \ +# --prop title.font=Calibri --prop title.size=16 \ +# --prop title.bold=true --prop title.color=1F4E79 \ +# --prop series.shadow=000000-3-315-1-30 \ +# --prop plotFill=F0F4F8 --prop chartFill=FFFFFF \ +# --prop legend=bottom +# +# Features: title.glow (color-radius-opacity), title.shadow, +# series.shadow on bar charts +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Advanced" --type chart' + f' --prop chartType=bar' + f' --prop title="Glow & Shadow Effects"' + f' --prop series1=East:185,195,210,228' + f' --prop series2=West:140,152,165,178' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop colors=4472C4,ED7D31' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop title.glow=4472C4-8-60' + f' --prop title.shadow=000000-3-315-2-40' + f' --prop title.font=Calibri --prop title.size=16' + f' --prop title.bold=true --prop title.color=1F4E79' + f' --prop series.shadow=000000-3-315-1-30' + f' --prop plotFill=F0F4F8 --prop chartFill=FFFFFF' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Error bars and data table +# +# officecli add charts-bar.xlsx "/6-Advanced" --type chart \ +# --prop chartType=bar \ +# --prop title="With Error Bars & Data Table" \ +# --prop dataRange=Sheet1!A1:E9 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=2E75B6,ED7D31,70AD47,FFC000 \ +# --prop errBars=percent:10 \ +# --prop dataTable=true \ +# --prop legend=none \ +# --prop plotFill=FAFAFA +# +# Features: errBars=percent:10, dataTable=true, legend=none +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Advanced" --type chart' + f' --prop chartType=bar' + f' --prop title="With Error Bars & Data Table"' + f' --prop dataRange=Sheet1!A1:E9' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=2E75B6,ED7D31,70AD47,FFC000' + f' --prop errBars=percent:10' + f' --prop dataTable=true' + f' --prop legend=none' + f' --prop plotFill=FAFAFA') + +print(f"\nDone! Generated: {FILE}") +print(" 7 sheets (Sheet1 data + 6 chart sheets, 24 charts total)") diff --git a/examples/excel/charts/charts-bar.xlsx b/examples/excel/charts/charts-bar.xlsx new file mode 100644 index 000000000..1332216b6 Binary files /dev/null and b/examples/excel/charts/charts-bar.xlsx differ diff --git a/examples/excel/charts/charts-basic.md b/examples/excel/charts/charts-basic.md new file mode 100644 index 000000000..513d8a49f --- /dev/null +++ b/examples/excel/charts/charts-basic.md @@ -0,0 +1,267 @@ +# Basic Charts Showcase + +This demo consists of three files that work together: + +- **charts-basic.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments, then executed by the script. +- **charts-basic.xlsx** — The generated workbook with 8 sheets (1 data + 7 chart sheets, 28 charts total). Open in Excel to see the rendered charts. +- **charts-basic.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-basic.py +# → charts-basic.xlsx +``` + +## Source Data + +**Sheet1**: 12 months of regional sales data (East, South, North, West) used by all charts. + +## Chart Sheets + +### Sheet: 1-Column Charts + +Four column chart variants demonstrating the column family. + +```bash +# Basic clustered column with axis titles and axis font +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop title="Regional Sales" \ + --prop dataRange=Sheet1!A1:E13 \ + --prop catTitle=Month --prop axisTitle=Sales \ + --prop axisfont=9:58626E:Arial \ + --prop gridlines=D9D9D9:0.5:dot + +# Stacked column with custom colors, data labels, gap control, series outline +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=columnStacked \ + --prop colors=2E75B6,70AD47,FFC000,C00000 \ + --prop dataLabels=true --prop labelPos=center \ + --prop gapwidth=60 \ + --prop series.outline=FFFFFF-0.5 + +# 100% stacked with legend positioning and plot fill +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=columnPercentStacked \ + --prop legend=bottom --prop legendfont=9:8B949E \ + --prop plotFill=F5F5F5 + +# 3D column with perspective and title styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column3d \ + --prop view3d=15,20,30 \ + --prop title.font=Calibri --prop title.size=16 \ + --prop title.color=1F4E79 --prop title.bold=true +``` + +**Features:** `column`, `columnStacked`, `columnPercentStacked`, `column3d`, `dataRange`, `catTitle`, `axisTitle`, `axisfont`, `gridlines`, `colors`, `dataLabels`, `labelPos`, `gapwidth`, `series.outline`, `legend`, `legendfont`, `plotFill`, `view3d`, `title.font/size/color/bold` + +### Sheet: 2-Bar Charts + +Four horizontal bar chart variants. + +```bash +# Horizontal bar with inline data and gap control +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar \ + --prop 'data=East:198;South:158;North:142;West:180' \ + --prop gapwidth=80 \ + --prop dataLabels=true --prop labelPos=outsideEnd + +# Stacked bar with named series and overlap +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=barStacked \ + --prop series1=H1:663,598,528,661 \ + --prop series2=H2:833,718,669,868 \ + --prop gapwidth=50 --prop overlap=0 + +# 100% stacked bar with reference line and axis lines +# Note: value axis of a barPercentStacked chart is 0-1 (= 0%-100%), so a 50% line = 0.5 +# referenceLine forms: value | value:color | value:color:label | value:color:width:dash +# | value:color:label:dash | value:color:width:dash:label +# Width is in points (default 1.5pt). e.g. 0.5:FF0000:2:dash draws a 2pt dashed line. +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=barPercentStacked \ + --prop referenceLine=0.5:FF0000:Target:dash \ + --prop axisLine=333333:1:solid \ + --prop catAxisLine=333333:1:solid + +# 3D bar with chart area fill and preset style +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bar3d \ + --prop view3d=10,30,20 \ + --prop chartFill=F2F2F2 \ + --prop style=3 +``` + +**Features:** `bar`, `barStacked`, `barPercentStacked`, `bar3d`, inline `data`, named `series`, `gapwidth`, `overlap`, `labelPos=outsideEnd`, `referenceLine`, `axisLine`, `catAxisLine`, `chartFill`, `style` + +### Sheet: 3-Line Charts + +Four line chart variants with markers, smoothing, and data tables. + +```bash +# Line with cell-range series (dotted syntax) and markers +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop series1.name=East \ + --prop series1.values=Sheet1!B2:B13 \ + --prop series1.categories=Sheet1!A2:A13 \ + --prop showMarkers=true --prop marker=circle:6:2E75B6 \ + --prop gridlines=D9D9D9:0.5:dot \ + --prop minorGridlines=EEEEEE:0.3:dot + +# Smooth line with series shadow +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop smooth=true --prop lineWidth=2.5 \ + --prop gridlines=none \ + --prop series.shadow=000000-4-315-2-40 + +# Stacked line with tick marks +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=lineStacked \ + --prop majorTickMark=outside --prop tickLabelPos=low + +# Dashed line with data table and hidden legend +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop lineDash=dash --prop lineWidth=1.5 \ + --prop dataTable=true --prop legend=none +``` + +**Features:** `series1.name/values/categories` (cell range), `showMarkers`, `marker` (style:size:color), `smooth`, `lineWidth`, `lineDash`, `gridlines`, `minorGridlines`, `series.shadow`, `lineStacked`, `majorTickMark`, `tickLabelPos`, `dataTable`, `legend=none` + +### Sheet: 4-Area Charts + +Four area chart variants with transparency and gradients. + +```bash +# Area with transparency and gradient +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area \ + --prop transparency=40 \ + --prop gradient=4472C4-BDD7EE:90 + +# Stacked area with plot fill and rounded corners +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=areaStacked \ + --prop plotFill=F5F5F5 --prop roundedCorners=true + +# 100% stacked area with axis visibility control +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=areaPercentStacked \ + --prop axisVisible=true --prop axisLine=999999:0.5:solid + +# 3D area with perspective +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=area3d \ + --prop view3d=20,25,15 +``` + +**Features:** `area`, `areaStacked`, `areaPercentStacked`, `area3d`, `transparency`, `gradient`, `plotFill`, `roundedCorners`, `axisVisible`, `axisLine` + +### Sheet: 5-Styling + +Demonstrates styling and formatting properties on various charts. + +```bash +# Fully styled chart: title effects, legend, axis fonts, series effects +officecli add data.xlsx /Sheet --type chart \ + --prop title.font=Georgia --prop title.size=18 \ + --prop title.color=1F4E79 --prop title.bold=true \ + --prop title.shadow=000000-3-315-2-30 \ + --prop legendfont=10:444444:Helvetica --prop legend=right \ + --prop axisfont=9:58626E:Arial \ + --prop series.outline=FFFFFF-0.5 \ + --prop series.shadow=000000-3-315-2-25 \ + --prop roundedCorners=true --prop referenceLine=160:FF0000:1:dash + +# Dual Y-axis (secondary axis) +officecli add data.xlsx /Sheet --type chart \ + --prop secondaryAxis=2 + +# Per-point coloring and negative value inversion +officecli add data.xlsx /Sheet --type chart \ + --prop point1.color=70AD47 --prop point3.color=FF0000 \ + --prop invertIfNeg=true + +# Gradient plot fill and custom data label text +officecli add data.xlsx /Sheet --type chart \ + --prop plotFill=E8F0FE-FFFFFF:90 \ + --prop marker=diamond:8:4472C4 \ + --prop dataLabels.numFmt=#,##0 \ + --prop dataLabel3.text=Peak! +``` + +**Features:** `title.shadow`, `secondaryAxis`, `point{N}.color`, `invertIfNeg`, `plotFill` gradient, `dataLabels.numFmt`, `dataLabel{N}.text` + +### Sheet: 6-Layout + +Manual positioning and axis control properties. + +```bash +# Manual layout of plot area, title, legend +officecli add data.xlsx /Sheet --type chart \ + --prop plotArea.x=0.15 --prop plotArea.y=0.15 \ + --prop plotArea.w=0.7 --prop plotArea.h=0.7 \ + --prop title.x=0.3 --prop title.y=0.01 \ + --prop legend.x=0.02 --prop legend.y=0.4 \ + --prop legend.overlay=true + +# Logarithmic scale, reversed axis, display units +officecli add data.xlsx /Sheet --type chart \ + --prop logBase=10 \ + --prop axisOrientation=maxMin \ + --prop dispUnits=thousands + +# Label font, separator, per-label hide +officecli add data.xlsx /Sheet --type chart \ + --prop labelFont=11:2E75B6:true \ + --prop "dataLabels.separator=: " \ + --prop dataLabel2.text=Best! \ + --prop dataLabel3.delete=true + +# Error bars, minor ticks, opacity +officecli add data.xlsx /Sheet --type chart \ + --prop errBars=percentage \ + --prop majorTickMark=outside --prop minorTickMark=inside \ + --prop opacity=80 +``` + +**Features:** `plotArea.x/y/w/h`, `title.x/y`, `legend.x/y`, `legend.overlay`, `logBase`, `axisOrientation`, `dispUnits`, `labelFont`, `dataLabels.separator`, `dataLabel{N}.delete`, `errBars`, `minorTickMark`, `opacity` + +### Sheet: 7-Effects + +Visual effects: gradients, conditional colors, glow, presets. + +```bash +# Per-series gradients +officecli add data.xlsx /Sheet --type chart \ + --prop 'gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90' + +# Area fill gradient and title glow +officecli add data.xlsx /Sheet --type chart \ + --prop areafill=4472C4-BDD7EE:90 \ + --prop title.glow=4472C4-8-60 + +# Conditional coloring (below/above threshold) +officecli add data.xlsx /Sheet --type chart \ + --prop colorRule=60:FF0000:70AD47 + +# Preset style and leader lines +officecli add data.xlsx /Sheet --type chart \ + --prop style=26 \ + --prop dataLabels.showLeaderLines=true +``` + +**Features:** `gradients`, `areafill`, `title.glow`, `colorRule`, `style`, `dataLabels.showLeaderLines` + +## Inspect the Generated File + +```bash +officecli query charts-basic.xlsx chart +officecli get charts-basic.xlsx "/1-Column Charts/chart[1]" +``` diff --git a/examples/excel/charts/charts-basic.py b/examples/excel/charts/charts-basic.py new file mode 100644 index 000000000..4a8112b53 --- /dev/null +++ b/examples/excel/charts/charts-basic.py @@ -0,0 +1,828 @@ +#!/usr/bin/env python3 +""" +Basic Charts Showcase — column, bar, line, and area charts with all variations. + +Generates: charts-basic.xlsx + +Each sheet demonstrates one chart family with all its variants and key properties. +See charts-basic.md for a guide to each sheet. + +Usage: + python3 charts-basic.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-basic.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Source data — shared across all charts +# ========================================================================== +print("\n--- Populating source data ---") + +data_cmds = [] +for j, h in enumerate(["Month", "East", "South", "North", "West"]): + data_cmds.append({"command": "set", "path": f"/Sheet1/{'ABCDE'[j]}1", "props": {"text": h, "bold": "true"}}) + +months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"] +east = [120, 135, 148, 162, 155, 178, 195, 210, 188, 172, 165, 198] +south = [95, 108, 115, 128, 142, 155, 168, 175, 160, 148, 135, 158] +north = [88, 92, 105, 118, 125, 138, 145, 152, 140, 130, 122, 142] +west = [110, 118, 130, 145, 138, 162, 175, 190, 170, 155, 148, 180] + +for i in range(12): + r = i + 2 + for j, val in enumerate([months[i], east[i], south[i], north[i], west[i]]): + data_cmds.append({"command": "set", "path": f"/Sheet1/{'ABCDE'[j]}{r}", "props": {"text": str(val)}}) + +cli(f'batch "{FILE}" --force --commands \'{json.dumps(data_cmds)}\'') + +# ========================================================================== +# Sheet: 1-Column Charts +# ========================================================================== +print("\n--- 1-Column Charts ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Column Charts"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic clustered column from cell range with axis titles +# +# officecli add charts-basic.xlsx "/1-Column Charts" --type chart \ +# --prop chartType=column \ +# --prop title="Regional Sales by Month" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop catTitle=Month --prop axisTitle=Sales \ +# --prop axisfont=9:58626E:Arial \ +# --prop gridlines=D9D9D9:0.5:dot +# +# Features: chartType=column, dataRange, catTitle, axisTitle, axisfont, gridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Column Charts" --type chart' + f' --prop chartType=column' + f' --prop title="Regional Sales by Month"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop catTitle=Month --prop axisTitle=Sales' + f' --prop axisfont=9:58626E:Arial' + f' --prop gridlines=D9D9D9:0.5:dot') + +# -------------------------------------------------------------------------- +# Chart 2: Stacked column with custom colors, data labels, and gap control +# +# officecli add charts-basic.xlsx "/1-Column Charts" --type chart \ +# --prop chartType=columnStacked \ +# --prop title="Stacked Regional Sales" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop colors=2E75B6,70AD47,FFC000,C00000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=center \ +# --prop gapwidth=60 \ +# --prop series.outline=FFFFFF-0.5 +# +# Features: columnStacked, colors, dataLabels, labelPos, gapwidth, series.outline +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Column Charts" --type chart' + f' --prop chartType=columnStacked' + f' --prop title="Stacked Regional Sales"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop colors=2E75B6,70AD47,FFC000,C00000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelPos=center' + f' --prop gapwidth=60' + f' --prop series.outline=FFFFFF-0.5') + +# -------------------------------------------------------------------------- +# Chart 3: 100% stacked column with legend position and plotFill +# +# officecli add charts-basic.xlsx "/1-Column Charts" --type chart \ +# --prop chartType=columnPercentStacked \ +# --prop title="Market Share by Month" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=bottom \ +# --prop legendfont=9:8B949E \ +# --prop plotFill=F5F5F5 +# +# Features: columnPercentStacked, legend=bottom, legendfont, plotFill +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Column Charts" --type chart' + f' --prop chartType=columnPercentStacked' + f' --prop title="Market Share by Month"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=bottom' + f' --prop legendfont=9:8B949E' + f' --prop plotFill=F5F5F5') + +# -------------------------------------------------------------------------- +# Chart 4: 3D column with perspective and title styling +# +# officecli add charts-basic.xlsx "/1-Column Charts" --type chart \ +# --prop chartType=column3d \ +# --prop title="3D Regional Sales" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=15,20,30 \ +# --prop title.font=Calibri --prop title.size=16 \ +# --prop title.color=1F4E79 --prop title.bold=true +# +# Features: column3d, view3d (rotX,rotY,perspective), title.font/size/color/bold +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Column Charts" --type chart' + f' --prop chartType=column3d' + f' --prop title="3D Regional Sales"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=15,20,30' + f' --prop title.font=Calibri --prop title.size=16' + f' --prop title.color=1F4E79 --prop title.bold=true') + +# ========================================================================== +# Sheet: 2-Bar Charts +# ========================================================================== +print("\n--- 2-Bar Charts ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Bar Charts"') + +# -------------------------------------------------------------------------- +# Chart 1: Horizontal bar with inline data and gapwidth +# +# officecli add charts-basic.xlsx "/2-Bar Charts" --type chart \ +# --prop chartType=bar \ +# --prop title="Q4 Sales by Region" \ +# --prop 'data=East:198;South:158;North:142;West:180' \ +# --prop categories=East,South,North,West \ +# --prop colors=2E75B6,70AD47,FFC000,C00000 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop gapwidth=80 \ +# --prop dataLabels=true --prop labelPos=outsideEnd +# +# Features: bar, inline data (Name:v1;Name2:v2), gapwidth, labelPos=outsideEnd +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bar Charts" --type chart' + f' --prop chartType=bar' + f' --prop title="Q4 Sales by Region"' + f' --prop "data=East:198;South:158;North:142;West:180"' + f' --prop categories=East,South,North,West' + f' --prop colors=2E75B6,70AD47,FFC000,C00000' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop gapwidth=80' + f' --prop dataLabels=true --prop labelPos=outsideEnd') + +# -------------------------------------------------------------------------- +# Chart 2: Stacked bar with named series and overlap +# +# officecli add charts-basic.xlsx "/2-Bar Charts" --type chart \ +# --prop chartType=barStacked \ +# --prop title="H1 vs H2 Sales" \ +# --prop series1=H1:663,598,528,661 \ +# --prop series2=H2:833,718,669,868 \ +# --prop categories=East,South,North,West \ +# --prop colors=4472C4,ED7D31 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=center \ +# --prop gapwidth=50 --prop overlap=0 +# +# Features: barStacked, named series (series1=Name:v1,v2), overlap +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bar Charts" --type chart' + f' --prop chartType=barStacked' + f' --prop title="H1 vs H2 Sales"' + f' --prop series1=H1:663,598,528,661' + f' --prop series2=H2:833,718,669,868' + f' --prop categories=East,South,North,West' + f' --prop colors=4472C4,ED7D31' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelPos=center' + f' --prop gapwidth=50 --prop overlap=0') + +# -------------------------------------------------------------------------- +# Chart 3: 100% stacked bar with reference line +# +# officecli add charts-basic.xlsx "/2-Bar Charts" --type chart \ +# --prop chartType=barPercentStacked \ +# --prop title="Regional Contribution %" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop referenceLine=0.5:FF0000:Target:dash \ +# --prop axisLine=333333:1:solid \ +# --prop catAxisLine=333333:1:solid +# +# Note: on a barPercentStacked chart, the value axis is 0-1 (displayed as 0%-100%), +# so a 50% reference line must be written as 0.5 — not 50. +# referenceLine supports: value | value:color | value:color:label | value:color:width:dash +# | value:color:label:dash (legacy) | value:color:width:dash:label (canonical). +# Width is in points; default 1.5pt. +# +# Features: barPercentStacked, referenceLine, axisLine, catAxisLine +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bar Charts" --type chart' + f' --prop chartType=barPercentStacked' + f' --prop title="Regional Contribution %"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop referenceLine=0.5:FF0000:Target:dash' + f' --prop axisLine=333333:1:solid' + f' --prop catAxisLine=333333:1:solid') + +# -------------------------------------------------------------------------- +# Chart 4: 3D bar with chart area fill and display units +# +# officecli add charts-basic.xlsx "/2-Bar Charts" --type chart \ +# --prop chartType=bar3d \ +# --prop title="3D Regional Comparison" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=10,30,20 \ +# --prop chartFill=F2F2F2 \ +# --prop style=3 +# +# Features: bar3d, chartFill (chart area background), style/styleId (preset 1-48) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bar Charts" --type chart' + f' --prop chartType=bar3d' + f' --prop title="3D Regional Comparison"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=10,30,20' + f' --prop chartFill=F2F2F2' + f' --prop style=3') + +# ========================================================================== +# Sheet: 3-Line Charts +# ========================================================================== +print("\n--- 3-Line Charts ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Line Charts"') + +# -------------------------------------------------------------------------- +# Chart 1: Line with markers and cell-range series (dotted syntax) +# +# officecli add charts-basic.xlsx "/3-Line Charts" --type chart \ +# --prop chartType=line \ +# --prop title="East Region Trend" \ +# --prop series1.name=East \ +# --prop series1.values=Sheet1!B2:B13 \ +# --prop series1.categories=Sheet1!A2:A13 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop showMarkers=true --prop marker=circle:6:2E75B6 \ +# --prop gridlines=D9D9D9:0.5:dot \ +# --prop minorGridlines=EEEEEE:0.3:dot +# +# Features: series.name/values/categories (cell range), marker (style:size:color), +# gridlines, minorGridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Line Charts" --type chart' + f' --prop chartType=line' + f' --prop title="East Region Trend"' + f' --prop series1.name=East' + f' --prop series1.values=Sheet1!B2:B13' + f' --prop series1.categories=Sheet1!A2:A13' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop showMarkers=true --prop marker=circle:6:2E75B6' + f' --prop gridlines=D9D9D9:0.5:dot' + f' --prop minorGridlines=EEEEEE:0.3:dot') + +# -------------------------------------------------------------------------- +# Chart 2: Smooth line with custom width and no gridlines +# +# officecli add charts-basic.xlsx "/3-Line Charts" --type chart \ +# --prop chartType=line \ +# --prop title="Smoothed Sales Trend" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop smooth=true --prop lineWidth=2.5 \ +# --prop colors=0070C0,00B050,FFC000,FF0000 \ +# --prop gridlines=none \ +# --prop series.shadow=000000-4-315-2-40 +# +# Features: smooth, lineWidth, gridlines=none, series.shadow (color-blur-angle-dist-opacity) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Line Charts" --type chart' + f' --prop chartType=line' + f' --prop title="Smoothed Sales Trend"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop smooth=true --prop lineWidth=2.5' + f' --prop colors=0070C0,00B050,FFC000,FF0000' + f' --prop gridlines=none' + f' --prop series.shadow=000000-4-315-2-40') + +# -------------------------------------------------------------------------- +# Chart 3: Stacked line +# +# officecli add charts-basic.xlsx "/3-Line Charts" --type chart \ +# --prop chartType=lineStacked \ +# --prop title="Cumulative Sales" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop catTitle=Month --prop axisTitle=Cumulative \ +# --prop majorTickMark=outside --prop tickLabelPos=low +# +# Features: lineStacked, majorTickMark, tickLabelPos +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Line Charts" --type chart' + f' --prop chartType=lineStacked' + f' --prop title="Cumulative Sales"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop catTitle=Month --prop axisTitle=Cumulative' + f' --prop majorTickMark=outside --prop tickLabelPos=low') + +# -------------------------------------------------------------------------- +# Chart 4: Line with dashed lines, data table, and hidden legend +# +# officecli add charts-basic.xlsx "/3-Line Charts" --type chart \ +# --prop chartType=line \ +# --prop title="Trend with Data Table" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop lineDash=dash --prop lineWidth=1.5 \ +# --prop dataTable=true \ +# --prop legend=none +# +# Features: lineDash (solid/dot/dash/dashdot/longdash), dataTable, legend=none +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Line Charts" --type chart' + f' --prop chartType=line' + f' --prop title="Trend with Data Table"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop lineDash=dash --prop lineWidth=1.5' + f' --prop dataTable=true' + f' --prop legend=none') + +# ========================================================================== +# Sheet: 4-Area Charts +# ========================================================================== +print("\n--- 4-Area Charts ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Area Charts"') + +# -------------------------------------------------------------------------- +# Chart 1: Area with transparency and gradient fill +# +# officecli add charts-basic.xlsx "/4-Area Charts" --type chart \ +# --prop chartType=area \ +# --prop title="Sales Volume" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop transparency=40 \ +# --prop gradient=4472C4-BDD7EE:90 +# +# Features: area, transparency (0-100%), gradient (color1-color2:angle) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Area Charts" --type chart' + f' --prop chartType=area' + f' --prop title="Sales Volume"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop transparency=40' + f' --prop gradient=4472C4-BDD7EE:90') + +# -------------------------------------------------------------------------- +# Chart 2: Stacked area with plotFill and rounded corners +# +# officecli add charts-basic.xlsx "/4-Area Charts" --type chart \ +# --prop chartType=areaStacked \ +# --prop title="Stacked Volume" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop plotFill=F5F5F5 \ +# --prop roundedCorners=true \ +# --prop transparency=30 +# +# Features: areaStacked, plotFill, roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Area Charts" --type chart' + f' --prop chartType=areaStacked' + f' --prop title="Stacked Volume"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop plotFill=F5F5F5' + f' --prop roundedCorners=true' + f' --prop transparency=30') + +# -------------------------------------------------------------------------- +# Chart 3: 100% stacked area with axis control +# +# officecli add charts-basic.xlsx "/4-Area Charts" --type chart \ +# --prop chartType=areaPercentStacked \ +# --prop title="Regional Mix %" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop transparency=20 \ +# --prop axisVisible=true \ +# --prop axisLine=999999:0.5:solid +# +# Features: areaPercentStacked, axisVisible, axisLine +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Area Charts" --type chart' + f' --prop chartType=areaPercentStacked' + f' --prop title="Regional Mix %"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop transparency=20' + f' --prop axisVisible=true' + f' --prop axisLine=999999:0.5:solid') + +# -------------------------------------------------------------------------- +# Chart 4: 3D area with perspective +# +# officecli add charts-basic.xlsx "/4-Area Charts" --type chart \ +# --prop chartType=area3d \ +# --prop title="3D Sales Volume" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=20,25,15 \ +# --prop colors=5B9BD5,A5D5A5,FFD966,F4B183 +# +# Features: area3d, view3d +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Area Charts" --type chart' + f' --prop chartType=area3d' + f' --prop title="3D Sales Volume"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=20,25,15' + f' --prop colors=5B9BD5,A5D5A5,FFD966,F4B183') + +# ========================================================================== +# Sheet: 5-Styling +# Demonstrates all styling/layout properties on a single column chart +# ========================================================================== +print("\n--- 5-Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="5-Styling"') + +# -------------------------------------------------------------------------- +# Chart 1: Fully styled column chart — title, legend, axis, series effects +# +# officecli add charts-basic.xlsx "/5-Styling" --type chart \ +# --prop chartType=column \ +# --prop title="Fully Styled Chart" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=0 --prop width=14 --prop height=20 \ +# --prop title.font=Georgia --prop title.size=18 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop title.shadow=000000-3-315-2-30 \ +# --prop legendfont=10:444444:Helvetica \ +# --prop legend=right \ +# --prop axisfont=9:58626E:Arial \ +# --prop catTitle=Month --prop axisTitle=Revenue \ +# --prop gridlines=CCCCCC:0.5:dot \ +# --prop plotFill=FAFAFA \ +# --prop chartFill=FFFFFF \ +# --prop series.outline=FFFFFF-0.5 \ +# --prop series.shadow=000000-3-315-2-25 \ +# --prop gapwidth=100 \ +# --prop roundedCorners=true \ +# --prop referenceLine=160:FF0000:1:dash \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 +# +# Features: title.font/size/color/bold/shadow, legendfont, axisfont, +# series.outline, series.shadow, roundedCorners, referenceLine +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Styling" --type chart' + f' --prop chartType=column' + f' --prop title="Fully Styled Chart"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=0 --prop width=14 --prop height=20' + f' --prop title.font=Georgia --prop title.size=18' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop title.shadow=000000-3-315-2-30' + f' --prop legendfont=10:444444:Helvetica' + f' --prop legend=right' + f' --prop axisfont=9:58626E:Arial' + f' --prop catTitle=Month --prop axisTitle=Revenue' + f' --prop gridlines=CCCCCC:0.5:dot' + f' --prop plotFill=FAFAFA' + f' --prop chartFill=FFFFFF' + f' --prop series.outline=FFFFFF-0.5' + f' --prop series.shadow=000000-3-315-2-25' + f' --prop gapwidth=100' + f' --prop roundedCorners=true' + f' --prop referenceLine=160:FF0000:1:dash' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000') + +# -------------------------------------------------------------------------- +# Chart 2: Column with secondary axis (dual Y-axis) +# +# officecli add charts-basic.xlsx "/5-Styling" --type chart \ +# --prop chartType=column \ +# --prop title="Sales vs Growth Rate" \ +# --prop series1=Sales:120,135,148,162 \ +# --prop series2=Growth:5.2,8.1,12.3,15.6 \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop x=15 --prop y=0 --prop width=10 --prop height=20 \ +# --prop secondaryAxis=2 \ +# --prop colors=4472C4,FF0000 +# +# Features: secondaryAxis (comma-separated 1-based series indices for second Y-axis) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Styling" --type chart' + f' --prop chartType=column' + f' --prop title="Sales vs Growth Rate"' + f' --prop series1=Sales:120,135,148,162' + f' --prop series2=Growth:5.2,8.1,12.3,15.6' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop x=15 --prop y=0 --prop width=10 --prop height=20' + f' --prop secondaryAxis=2' + f' --prop colors=4472C4,FF0000') + +# -------------------------------------------------------------------------- +# Chart 3: Column with individual point colors and inverted negatives +# +# officecli add charts-basic.xlsx "/5-Styling" --type chart \ +# --prop chartType=column \ +# --prop title="Quarterly P&L" \ +# --prop series1=P&L:500,300,-200,800 \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop x=0 --prop y=21 --prop width=10 --prop height=18 \ +# --prop point1.color=70AD47 --prop point2.color=70AD47 \ +# --prop point3.color=FF0000 --prop point4.color=70AD47 \ +# --prop invertIfNeg=true \ +# --prop dataLabels=true --prop labelPos=outsideEnd +# +# Features: point{N}.color (per-point coloring), invertIfNeg +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Styling" --type chart' + f' --prop chartType=column' + f' --prop title="Quarterly P&L"' + f' --prop "series1=P&L:500,300,-200,800"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop x=0 --prop y=21 --prop width=10 --prop height=18' + f' --prop point1.color=70AD47 --prop point2.color=70AD47' + f' --prop point3.color=FF0000 --prop point4.color=70AD47' + f' --prop invertIfNeg=true' + f' --prop dataLabels=true --prop labelPos=outsideEnd') + +# -------------------------------------------------------------------------- +# Chart 4: Line with gradient plot area and custom data labels +# +# officecli add charts-basic.xlsx "/5-Styling" --type chart \ +# --prop chartType=line \ +# --prop title="Custom Labels Demo" \ +# --prop series1=Revenue:100,200,300,250 \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop x=11 --prop y=21 --prop width=14 --prop height=18 \ +# --prop plotFill=E8F0FE-FFFFFF:90 \ +# --prop showMarkers=true --prop marker=diamond:8:4472C4 \ +# --prop lineWidth=2 \ +# --prop dataLabels=true --prop labelPos=top \ +# --prop dataLabels.numFmt=#,##0 \ +# --prop dataLabel3.text=Peak! +# +# Features: plotFill gradient (color1-color2:angle), marker styles (diamond), +# dataLabels.numFmt, dataLabel{N}.text (custom text for one label) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Styling" --type chart' + f' --prop chartType=line' + f' --prop title="Custom Labels Demo"' + f' --prop series1=Revenue:100,200,300,250' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop x=11 --prop y=21 --prop width=14 --prop height=18' + f' --prop plotFill=E8F0FE-FFFFFF:90' + f' --prop showMarkers=true --prop marker=diamond:8:4472C4' + f' --prop lineWidth=2' + f' --prop dataLabels=true --prop labelPos=top' + f' --prop dataLabels.numFmt=#,##0' + f' --prop dataLabel3.text=Peak!') + +# ========================================================================== +# Sheet: 6-Layout +# Manual layout of plot area, title, legend; axis orientation; log scale; +# display units; label font and separator; error bars +# ========================================================================== +print("\n--- 6-Layout ---") +cli(f'add "{FILE}" / --type sheet --prop name="6-Layout"') + +# -------------------------------------------------------------------------- +# Chart 1: Manual layout positioning of plot area, title, legend +# +# officecli add charts-basic.xlsx "/6-Layout" --type chart \ +# --prop chartType=column \ +# --prop title="Manual Layout" \ +# --prop dataRange=Sheet1!A1:C13 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop plotArea.x=0.15 --prop plotArea.y=0.15 \ +# --prop plotArea.w=0.7 --prop plotArea.h=0.7 \ +# --prop title.x=0.3 --prop title.y=0.01 \ +# --prop legend.x=0.02 --prop legend.y=0.4 \ +# --prop legend.overlay=true +# +# Features: plotArea.x/y/w/h (0-1 fraction), title.x/y, legend.x/y, legend.overlay +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Layout" --type chart' + f' --prop chartType=column' + f' --prop title="Manual Layout"' + f' --prop dataRange=Sheet1!A1:C13' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop plotArea.x=0.15 --prop plotArea.y=0.15' + f' --prop plotArea.w=0.7 --prop plotArea.h=0.7' + f' --prop title.x=0.3 --prop title.y=0.01' + f' --prop legend.x=0.02 --prop legend.y=0.4' + f' --prop legend.overlay=true') + +# -------------------------------------------------------------------------- +# Chart 2: Reversed axis, log scale, display units +# +# officecli add charts-basic.xlsx "/6-Layout" --type chart \ +# --prop chartType=bar \ +# --prop title="Log Scale + Reversed Axis" \ +# --prop series1=Revenue:10,100,1000,10000 \ +# --prop categories=Startup,Small,Medium,Enterprise \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop logBase=10 \ +# --prop axisOrientation=maxMin \ +# --prop dispUnits=thousands +# +# Features: logBase (logarithmic scale), axisOrientation=maxMin (reversed), +# dispUnits (thousands/millions) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Layout" --type chart' + f' --prop chartType=bar' + f' --prop title="Log Scale + Reversed Axis"' + f' --prop series1=Revenue:10,100,1000,10000' + f' --prop categories=Startup,Small,Medium,Enterprise' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop logBase=10' + f' --prop axisOrientation=maxMin' + f' --prop dispUnits=thousands') + +# -------------------------------------------------------------------------- +# Chart 3: Label font, separator, leader lines, and per-label layout +# +# officecli add charts-basic.xlsx "/6-Layout" --type chart \ +# --prop chartType=column \ +# --prop title="Label Formatting" \ +# --prop series1=Sales:120,200,150,180 \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=outsideEnd \ +# --prop labelFont=11:2E75B6:true \ +# --prop dataLabels.separator=": " \ +# --prop dataLabel2.text=Best! \ +# --prop dataLabel3.delete=true +# +# Features: labelFont (size:color:bold), dataLabels.separator, +# dataLabel{N}.text (custom), dataLabel{N}.delete (hide one label) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Layout" --type chart' + f' --prop chartType=column' + f' --prop title="Label Formatting"' + f' --prop series1=Sales:120,200,150,180' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelPos=outsideEnd' + f' --prop labelFont=11:2E75B6:true' + f' --prop "dataLabels.separator=: "' + f' --prop dataLabel2.text=Best!' + f' --prop dataLabel3.delete=true') + +# -------------------------------------------------------------------------- +# Chart 4: Error bars, minor ticks, opacity +# +# officecli add charts-basic.xlsx "/6-Layout" --type chart \ +# --prop chartType=line \ +# --prop title="Error Bars + Ticks" \ +# --prop series1=Measurement:50,55,48,62,58 \ +# --prop categories=Mon,Tue,Wed,Thu,Fri \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop showMarkers=true --prop marker=square:7:4472C4 \ +# --prop errBars=percentage \ +# --prop majorTickMark=outside --prop minorTickMark=inside \ +# --prop opacity=80 +# +# Features: errBars (percentage/stdDev/fixed), minorTickMark, opacity (0-100%) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Layout" --type chart' + f' --prop chartType=line' + f' --prop title="Error Bars + Ticks"' + f' --prop series1=Measurement:50,55,48,62,58' + f' --prop categories=Mon,Tue,Wed,Thu,Fri' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop showMarkers=true --prop marker=square:7:4472C4' + f' --prop errBars=percentage' + f' --prop majorTickMark=outside --prop minorTickMark=inside' + f' --prop opacity=80') + +# ========================================================================== +# Sheet: 7-Effects +# Gradients, conditional color, area fill, title glow, preset themes +# ========================================================================== +print("\n--- 7-Effects ---") +cli(f'add "{FILE}" / --type sheet --prop name="7-Effects"') + +# -------------------------------------------------------------------------- +# Chart 1: Per-series gradients +# +# officecli add charts-basic.xlsx "/7-Effects" --type chart \ +# --prop chartType=column \ +# --prop title="Per-Series Gradients" \ +# --prop series1=East:120,135,148 \ +# --prop series2=West:110,118,130 \ +# --prop categories=Q1,Q2,Q3 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop 'gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90' +# +# Features: gradients (per-series, semicolon-separated "C1-C2:angle") +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Effects" --type chart' + f' --prop chartType=column' + f' --prop title="Per-Series Gradients"' + f' --prop series1=East:120,135,148' + f' --prop series2=West:110,118,130' + f' --prop categories=Q1,Q2,Q3' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop "gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90"') + +# -------------------------------------------------------------------------- +# Chart 2: Area fill gradient and title glow effect +# +# officecli add charts-basic.xlsx "/7-Effects" --type chart \ +# --prop chartType=area \ +# --prop title="Glow Title + Area Fill" \ +# --prop dataRange=Sheet1!A1:C13 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop areafill=4472C4-BDD7EE:90 \ +# --prop transparency=30 \ +# --prop title.glow=4472C4-8-60 \ +# --prop title.size=16 +# +# Features: areafill (area gradient), title.glow (color-radius-opacity) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Effects" --type chart' + f' --prop chartType=area' + f' --prop title="Glow Title + Area Fill"' + f' --prop dataRange=Sheet1!A1:C13' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop areafill=4472C4-BDD7EE:90' + f' --prop transparency=30' + f' --prop title.glow=4472C4-8-60' + f' --prop title.size=16') + +# -------------------------------------------------------------------------- +# Chart 3: Conditional coloring rule +# +# officecli add charts-basic.xlsx "/7-Effects" --type chart \ +# --prop chartType=column \ +# --prop title="Conditional Colors" \ +# --prop series1=Score:85,42,91,38,76,55 \ +# --prop categories=A,B,C,D,E,F \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colorRule=60:FF0000:70AD47 \ +# --prop dataLabels=true --prop labelPos=outsideEnd +# +# Features: colorRule (threshold:belowColor:aboveColor — values below 60 red, above green) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Effects" --type chart' + f' --prop chartType=column' + f' --prop title="Conditional Colors"' + f' --prop series1=Score:85,42,91,38,76,55' + f' --prop categories=A,B,C,D,E,F' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop colorRule=60:FF0000:70AD47' + f' --prop dataLabels=true --prop labelPos=outsideEnd') + +# -------------------------------------------------------------------------- +# Chart 4: Preset style/theme and leader lines +# +# officecli add charts-basic.xlsx "/7-Effects" --type chart \ +# --prop chartType=column \ +# --prop title="Preset Style 26" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop style=26 \ +# --prop dataLabels=true \ +# --prop dataLabels.showLeaderLines=true +# +# Features: style (preset 1-48), dataLabels.showLeaderLines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Effects" --type chart' + f' --prop chartType=column' + f' --prop title="Preset Style 26"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop style=26' + f' --prop dataLabels=true' + f' --prop dataLabels.showLeaderLines=true') + +print(f"\nDone! Generated: {FILE}") +print(" 8 sheets (Sheet1 data + 7 chart sheets, 28 charts total)") diff --git a/examples/excel/charts/charts-basic.xlsx b/examples/excel/charts/charts-basic.xlsx new file mode 100644 index 000000000..195487c4b Binary files /dev/null and b/examples/excel/charts/charts-basic.xlsx differ diff --git a/examples/excel/charts/charts-boxwhisker.md b/examples/excel/charts/charts-boxwhisker.md new file mode 100644 index 000000000..064f702fe --- /dev/null +++ b/examples/excel/charts/charts-boxwhisker.md @@ -0,0 +1,181 @@ +# Box-Whisker Chart Showcase + +This demo consists of three files that work together: + +- **charts-boxwhisker.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-boxwhisker.xlsx** — The generated workbook with 2 sheets (8 box-whisker charts total). +- **charts-boxwhisker.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-boxwhisker.py +# → charts-boxwhisker.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Basics & Quartile + +Four box-whisker charts covering basic usage, quartile methods, title styling, and series colors. + +```bash +# Chart 1: Single series, exclusive quartile, data labels +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=boxWhisker \ + --prop title="Test Score Distribution" \ + --prop series1="Scores:45,52,58,61,63,65,67,68,70,72,75,78,82,85,90,95,99" \ + --prop quartileMethod=exclusive \ + --prop dataLabels=true + +# Chart 2: Three-series comparison, inclusive quartile, legend +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=boxWhisker \ + --prop title="Salary by Department ($k)" \ + --prop series1="Engineering:85,92,95,98,102,105,108,112,118,125,135,150,180" \ + --prop series2="Marketing:60,65,68,72,75,78,80,83,88,92,98,110" \ + --prop series3="Sales:55,62,68,75,82,90,98,105,115,125,140,160,190" \ + --prop quartileMethod=inclusive \ + --prop legend=bottom + +# Chart 3: Title styling — color, size, bold, font, shadow +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=boxWhisker \ + --prop title="Styled Title Demo" \ + --prop title.color=1B2838 --prop title.size=20 \ + --prop title.bold=true --prop title.font=Georgia \ + --prop title.shadow=000000-6-45-3-50 \ + --prop series1="Data:18,22,25,28,30,32,35,38,40,42,45,55,62,78" + +# Chart 4: Per-series colors and drop shadow +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=boxWhisker \ + --prop title="Custom Series Colors" \ + --prop series1="GroupA:30,38,45,52,58,62,65,68,71,74,78,85,92" \ + --prop series2="GroupB:20,28,35,40,48,55,60,66,70,80,88,95,110" \ + --prop colors=5B9BD5,ED7D31 \ + --prop series.shadow=000000-6-45-3-35 +``` + +**Features:** `quartileMethod=exclusive`, `quartileMethod=inclusive`, `dataLabels`, `legend=bottom`, multi-series (3), `title.color`, `title.size`, `title.bold`, `title.font`, `title.shadow`, `colors` (per-series), `series.shadow` + +### Sheet: 2-Axes & Styling + +Four box-whisker charts covering axis control, gridlines, area fills, and a full presentation-grade chart. + +```bash +# Chart 5: Axis scaling, axis titles, axis title styling, axis font +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=boxWhisker \ + --prop title="Response Time (ms)" \ + --prop series1="API:12,18,22,25,28,30,32,35,38,40,42,45,55,62,78,95,120" \ + --prop series2="DB:5,8,10,12,14,16,18,20,22,25,28,32,38,45,60" \ + --prop axismin=0 --prop axismax=130 \ + --prop majorunit=10 --prop minorunit=5 \ + --prop xAxisTitle="Service" --prop yAxisTitle="Latency (ms)" \ + --prop axisTitle.color=4A5568 --prop axisTitle.size=12 \ + --prop axisTitle.bold=true --prop axisTitle.font="Helvetica Neue" \ + --prop "axisfont=10:6B7280:Consolas" + +# Chart 6: Axis visibility, axis lines, gridlines, cross-axis gridlines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=boxWhisker \ + --prop title="Axis & Gridline Control" \ + --prop series1="Temp:15,18,20,22,24,26,28,30,32,35,38,40,42" \ + --prop cataxis.visible=false \ + --prop "valaxis.line=334155:1.5" \ + --prop gridlines=true --prop gridlineColor=E2E8F0 \ + --prop xGridlines=true --prop xGridlineColor=F1F5F9 + +# Chart 7: Card style — area fills/borders, gapWidth, no tick labels +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=boxWhisker \ + --prop title="Card Style" \ + --prop series1="Weight:50,55,58,60,62,64,66,68,70,72,75,78,82,88,95" \ + --prop fill=6366F1 \ + --prop gapWidth=200 \ + --prop tickLabels=false --prop gridlines=false \ + --prop plotareafill=F8FAFC --prop "plotarea.border=E2E8F0:1" \ + --prop chartareafill=FFFFFF --prop "chartarea.border=CBD5E1:0.75" + +# Chart 8: Full presentation-grade — all properties combined +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=boxWhisker \ + --prop title="Server Latency Dashboard" \ + --prop title.color=0F172A --prop title.size=18 \ + --prop title.bold=true --prop title.font="Helvetica Neue" \ + --prop title.shadow=000000-4-45-2-40 \ + --prop series1="US-East:8,12,15,18,20,22,24,26,28,30,35,42,55,70,95" \ + --prop series2="EU-West:10,14,18,22,25,28,30,33,36,40,45,50,60,80" \ + --prop series3="AP-South:15,20,25,30,35,38,42,45,48,52,58,65,75,90,120" \ + --prop quartileMethod=exclusive \ + --prop colors=3B82F6,10B981,F59E0B \ + --prop series.shadow=000000-4-45-2-30 \ + --prop axismin=0 --prop axismax=130 --prop majorunit=10 \ + --prop xAxisTitle="Region" --prop yAxisTitle="Latency (ms)" \ + --prop axisTitle.color=475569 --prop axisTitle.size=11 \ + --prop axisTitle.bold=true --prop axisTitle.font="Helvetica Neue" \ + --prop "axisfont=9:64748B:Helvetica Neue" \ + --prop "axisline=CBD5E1:1" \ + --prop gridlineColor=E2E8F0 \ + --prop dataLabels=true --prop "datalabels.numfmt=0" \ + --prop legend=top --prop legend.overlay=false \ + --prop "legendfont=10:475569:Helvetica Neue" \ + --prop plotareafill=F8FAFC --prop "plotarea.border=E2E8F0:0.75" \ + --prop chartareafill=FFFFFF --prop "chartarea.border=CBD5E1:0.75" +``` + +**Features:** `axismin`, `axismax`, `majorunit`, `minorunit`, `xAxisTitle`, `yAxisTitle`, `axisTitle.color`, `axisTitle.size`, `axisTitle.bold`, `axisTitle.font`, `axisfont`, `cataxis.visible`, `valaxis.line`, `gridlines`, `gridlineColor`, `xGridlines`, `xGridlineColor`, `fill` (single color), `gapWidth`, `tickLabels`, `plotareafill`, `plotarea.border`, `chartareafill`, `chartarea.border`, `axisline`, `datalabels.numfmt`, `legend.overlay`, `legendfont` + +## Property Coverage + +| Property | Chart | +|---|---| +| `chartType=boxWhisker` | 1-8 | +| `quartileMethod=exclusive` | 1, 8 | +| `quartileMethod=inclusive` | 2 | +| `dataLabels` | 1, 8 | +| `datalabels.numfmt` | 8 | +| `legend=bottom` | 2 | +| `legend=top` | 8 | +| `legend.overlay` | 8 | +| `legendfont` | 8 | +| `title.color` | 3, 8 | +| `title.size` | 3, 8 | +| `title.bold` | 3, 8 | +| `title.font` | 3, 8 | +| `title.shadow` | 3, 8 | +| `fill` (single color) | 7 | +| `colors` (per-series) | 4, 8 | +| `series.shadow` | 4, 8 | +| `axismin` / `axismax` | 5, 8 | +| `majorunit` | 5, 8 | +| `minorunit` | 5 | +| `xAxisTitle` | 5, 8 | +| `yAxisTitle` | 5, 8 | +| `axisTitle.color` | 5, 8 | +| `axisTitle.size` | 5, 8 | +| `axisTitle.bold` | 5, 8 | +| `axisTitle.font` | 5, 8 | +| `axisfont` | 5, 8 | +| `cataxis.visible` | 6 | +| `valaxis.line` | 6 | +| `axisline` | 8 | +| `gridlines` | 6, 7 | +| `gridlineColor` | 6, 8 | +| `xGridlines` | 6 | +| `xGridlineColor` | 6 | +| `tickLabels` | 7 | +| `gapWidth` | 7 | +| `plotareafill` | 7, 8 | +| `plotarea.border` | 7, 8 | +| `chartareafill` | 7, 8 | +| `chartarea.border` | 7, 8 | + +## Inspect the Generated File + +```bash +officecli query charts-boxwhisker.xlsx chart +officecli get charts-boxwhisker.xlsx "/1-Basics & Quartile/chart[1]" +``` diff --git a/examples/excel/charts/charts-boxwhisker.py b/examples/excel/charts/charts-boxwhisker.py new file mode 100644 index 000000000..59c7f7ffa --- /dev/null +++ b/examples/excel/charts/charts-boxwhisker.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +""" +Box-Whisker Chart Showcase — all boxWhisker properties. + +Generates: charts-boxwhisker.xlsx + +Usage: + python3 charts-boxwhisker.py +""" + +import subprocess, sys, os, atexit + +FILE = "charts-boxwhisker.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Sheet 1: Basics & Quartile Methods +# ========================================================================== +print("\n--- 1-Basics & Quartile ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Basics & Quartile"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic single-series with exclusive quartile and data labels +# +# officecli add charts-boxwhisker.xlsx "/1-Basics & Quartile" --type chart \ +# --prop chartType=boxWhisker \ +# --prop title="Test Score Distribution" \ +# --prop series1="Scores:45,52,58,61,63,65,67,68,70,72,75,78,82,85,90,95,99" \ +# --prop quartileMethod=exclusive \ +# --prop dataLabels=true \ +# --prop x=0 --prop y=0 --prop width=13 --prop height=18 +# +# Features: single series, quartileMethod=exclusive, dataLabels +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Basics & Quartile" --type chart' + f' --prop chartType=boxWhisker' + f' --prop title="Test Score Distribution"' + f' --prop "series1=Scores:45,52,58,61,63,65,67,68,70,72,75,78,82,85,90,95,99"' + f' --prop quartileMethod=exclusive' + f' --prop dataLabels=true' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 2: Multi-series with inclusive quartile, legend at bottom +# +# officecli add charts-boxwhisker.xlsx "/1-Basics & Quartile" --type chart \ +# --prop chartType=boxWhisker \ +# --prop title="Salary by Department ($k)" \ +# --prop series1="Engineering:85,92,95,98,102,105,108,112,118,125,135,150,180" \ +# --prop series2="Marketing:60,65,68,72,75,78,80,83,88,92,98,110" \ +# --prop series3="Sales:55,62,68,75,82,90,98,105,115,125,140,160,190" \ +# --prop quartileMethod=inclusive \ +# --prop legend=bottom \ +# --prop x=14 --prop y=0 --prop width=13 --prop height=18 +# +# Features: 3 series, quartileMethod=inclusive, legend=bottom +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Basics & Quartile" --type chart' + f' --prop chartType=boxWhisker' + f' --prop title="Salary by Department (\\$k)"' + f' --prop "series1=Engineering:85,92,95,98,102,105,108,112,118,125,135,150,180"' + f' --prop "series2=Marketing:60,65,68,72,75,78,80,83,88,92,98,110"' + f' --prop "series3=Sales:55,62,68,75,82,90,98,105,115,125,140,160,190"' + f' --prop quartileMethod=inclusive' + f' --prop legend=bottom' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 3: Title styling — color, size, bold, font, shadow +# +# officecli add charts-boxwhisker.xlsx "/1-Basics & Quartile" --type chart \ +# --prop chartType=boxWhisker \ +# --prop title="Styled Title Demo" \ +# --prop title.color=1B2838 \ +# --prop title.size=20 \ +# --prop title.bold=true \ +# --prop title.font="Georgia" \ +# --prop title.shadow=000000-6-45-3-50 \ +# --prop series1="Data:18,22,25,28,30,32,35,38,40,42,45,55,62,78" \ +# --prop x=0 --prop y=19 --prop width=13 --prop height=18 +# +# Features: title.color, title.size, title.bold, title.font, title.shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Basics & Quartile" --type chart' + f' --prop chartType=boxWhisker' + f' --prop title="Styled Title Demo"' + f' --prop title.color=1B2838' + f' --prop title.size=20' + f' --prop title.bold=true' + f' --prop title.font=Georgia' + f' --prop title.shadow=000000-6-45-3-50' + f' --prop "series1=Data:18,22,25,28,30,32,35,38,40,42,45,55,62,78"' + f' --prop x=0 --prop y=19 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 4: Series colors — fill, colors (per-series), series.shadow +# +# officecli add charts-boxwhisker.xlsx "/1-Basics & Quartile" --type chart \ +# --prop chartType=boxWhisker \ +# --prop title="Custom Series Colors" \ +# --prop series1="GroupA:30,38,45,52,58,62,65,68,71,74,78,85,92" \ +# --prop series2="GroupB:20,28,35,40,48,55,60,66,70,80,88,95,110" \ +# --prop colors=5B9BD5,ED7D31 \ +# --prop series.shadow=000000-6-45-3-35 \ +# --prop x=14 --prop y=19 --prop width=13 --prop height=18 +# +# Features: colors (per-series hex), series.shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Basics & Quartile" --type chart' + f' --prop chartType=boxWhisker' + f' --prop title="Custom Series Colors"' + f' --prop "series1=GroupA:30,38,45,52,58,62,65,68,71,74,78,85,92"' + f' --prop "series2=GroupB:20,28,35,40,48,55,60,66,70,80,88,95,110"' + f' --prop colors=5B9BD5,ED7D31' + f' --prop series.shadow=000000-6-45-3-35' + f' --prop x=14 --prop y=19 --prop width=13 --prop height=18') + +# ========================================================================== +# Sheet 2: Axes & Styling +# ========================================================================== +print("\n--- 2-Axes & Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Axes & Styling"') + +# -------------------------------------------------------------------------- +# Chart 5: Axis scaling + axis titles + axis title styling + axis font +# +# officecli add charts-boxwhisker.xlsx "/2-Axes & Styling" --type chart \ +# --prop chartType=boxWhisker \ +# --prop title="Response Time (ms)" \ +# --prop series1="API:12,18,22,25,28,30,32,35,38,40,42,45,55,62,78,95,120" \ +# --prop series2="DB:5,8,10,12,14,16,18,20,22,25,28,32,38,45,60" \ +# --prop axismin=0 --prop axismax=130 --prop majorunit=10 --prop minorunit=5 \ +# --prop xAxisTitle="Service" \ +# --prop yAxisTitle="Latency (ms)" \ +# --prop axisTitle.color=4A5568 \ +# --prop axisTitle.size=12 \ +# --prop axisTitle.bold=true \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop axisfont=10:6B7280:Consolas \ +# --prop x=0 --prop y=0 --prop width=13 --prop height=18 +# +# Features: axismin, axismax, majorunit, minorunit, xAxisTitle, yAxisTitle, +# axisTitle.color/.size/.bold/.font, axisfont +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Axes & Styling" --type chart' + f' --prop chartType=boxWhisker' + f' --prop title="Response Time (ms)"' + f' --prop "series1=API:12,18,22,25,28,30,32,35,38,40,42,45,55,62,78,95,120"' + f' --prop "series2=DB:5,8,10,12,14,16,18,20,22,25,28,32,38,45,60"' + f' --prop axismin=0 --prop axismax=130 --prop majorunit=10 --prop minorunit=5' + f' --prop xAxisTitle=Service' + f' --prop yAxisTitle="Latency (ms)"' + f' --prop axisTitle.color=4A5568' + f' --prop axisTitle.size=12' + f' --prop axisTitle.bold=true' + f' --prop axisTitle.font="Helvetica Neue"' + f' --prop "axisfont=10:6B7280:Consolas"' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 6: Axis visibility + axis lines + gridlines + xGridlines +# +# officecli add charts-boxwhisker.xlsx "/2-Axes & Styling" --type chart \ +# --prop chartType=boxWhisker \ +# --prop title="Axis & Gridline Control" \ +# --prop series1="Temp:15,18,20,22,24,26,28,30,32,35,38,40,42" \ +# --prop cataxis.visible=false \ +# --prop valaxis.line=334155:1.5 \ +# --prop gridlines=true \ +# --prop gridlineColor=E2E8F0 \ +# --prop xGridlines=true \ +# --prop xGridlineColor=F1F5F9 \ +# --prop x=14 --prop y=0 --prop width=13 --prop height=18 +# +# Features: cataxis.visible=false, valaxis.line, gridlines, gridlineColor, +# xGridlines, xGridlineColor +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Axes & Styling" --type chart' + f' --prop chartType=boxWhisker' + f' --prop title="Axis & Gridline Control"' + f' --prop "series1=Temp:15,18,20,22,24,26,28,30,32,35,38,40,42"' + f' --prop cataxis.visible=false' + f' --prop "valaxis.line=334155:1.5"' + f' --prop gridlines=true' + f' --prop gridlineColor=E2E8F0' + f' --prop xGridlines=true' + f' --prop xGridlineColor=F1F5F9' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 7: Plot/chart area fills, borders, gapWidth, tickLabels=false +# +# officecli add charts-boxwhisker.xlsx "/2-Axes & Styling" --type chart \ +# --prop chartType=boxWhisker \ +# --prop title="Card Style" \ +# --prop series1="Weight:50,55,58,60,62,64,66,68,70,72,75,78,82,88,95" \ +# --prop fill=6366F1 \ +# --prop gapWidth=200 \ +# --prop tickLabels=false \ +# --prop gridlines=false \ +# --prop plotareafill=F8FAFC \ +# --prop plotarea.border=E2E8F0:1 \ +# --prop chartareafill=FFFFFF \ +# --prop chartarea.border=CBD5E1:0.75 \ +# --prop x=0 --prop y=19 --prop width=13 --prop height=18 +# +# Features: fill (single color), gapWidth, tickLabels=false, gridlines=false, +# plotareafill, plotarea.border, chartareafill, chartarea.border +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Axes & Styling" --type chart' + f' --prop chartType=boxWhisker' + f' --prop title="Card Style"' + f' --prop "series1=Weight:50,55,58,60,62,64,66,68,70,72,75,78,82,88,95"' + f' --prop fill=6366F1' + f' --prop gapWidth=200' + f' --prop tickLabels=false' + f' --prop gridlines=false' + f' --prop plotareafill=F8FAFC' + f' --prop "plotarea.border=E2E8F0:1"' + f' --prop chartareafill=FFFFFF' + f' --prop "chartarea.border=CBD5E1:0.75"' + f' --prop x=0 --prop y=19 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 8: Full presentation-grade — everything combined +# +# officecli add charts-boxwhisker.xlsx "/2-Axes & Styling" --type chart \ +# --prop chartType=boxWhisker \ +# --prop title="Server Latency Dashboard" \ +# --prop title.color=0F172A \ +# --prop title.size=18 \ +# --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop title.shadow=000000-4-45-2-40 \ +# --prop series1="US-East:8,12,15,18,20,22,24,26,28,30,35,42,55,70,95" \ +# --prop series2="EU-West:10,14,18,22,25,28,30,33,36,40,45,50,60,80" \ +# --prop series3="AP-South:15,20,25,30,35,38,42,45,48,52,58,65,75,90,120" \ +# --prop quartileMethod=exclusive \ +# --prop colors=3B82F6,10B981,F59E0B \ +# --prop series.shadow=000000-4-45-2-30 \ +# --prop axismin=0 --prop axismax=130 --prop majorunit=10 \ +# --prop xAxisTitle="Region" \ +# --prop yAxisTitle="Latency (ms)" \ +# --prop axisTitle.color=475569 \ +# --prop axisTitle.size=11 \ +# --prop axisTitle.bold=true \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop axisfont=9:64748B:Helvetica\ Neue \ +# --prop axisline=CBD5E1:1 \ +# --prop gridlineColor=E2E8F0 \ +# --prop dataLabels=true \ +# --prop datalabels.numfmt=0 \ +# --prop legend=top \ +# --prop legend.overlay=false \ +# --prop legendfont=10:475569:Helvetica\ Neue \ +# --prop plotareafill=F8FAFC \ +# --prop plotarea.border=E2E8F0:0.75 \ +# --prop chartareafill=FFFFFF \ +# --prop chartarea.border=CBD5E1:0.75 \ +# --prop x=14 --prop y=19 --prop width=16 --prop height=22 +# +# Features: ALL properties combined — title styling, multi-series colors, +# series.shadow, axis scaling, axis titles + styling, axisfont, axisline, +# gridlineColor, dataLabels + numfmt, legend + overlay + legendfont, +# plot/chart area fill + border +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Axes & Styling" --type chart' + f' --prop chartType=boxWhisker' + f' --prop title="Server Latency Dashboard"' + f' --prop title.color=0F172A' + f' --prop title.size=18' + f' --prop title.bold=true' + f' --prop title.font="Helvetica Neue"' + f' --prop title.shadow=000000-4-45-2-40' + f' --prop "series1=US-East:8,12,15,18,20,22,24,26,28,30,35,42,55,70,95"' + f' --prop "series2=EU-West:10,14,18,22,25,28,30,33,36,40,45,50,60,80"' + f' --prop "series3=AP-South:15,20,25,30,35,38,42,45,48,52,58,65,75,90,120"' + f' --prop quartileMethod=exclusive' + f' --prop colors=3B82F6,10B981,F59E0B' + f' --prop series.shadow=000000-4-45-2-30' + f' --prop axismin=0 --prop axismax=130 --prop majorunit=10' + f' --prop xAxisTitle=Region' + f' --prop yAxisTitle="Latency (ms)"' + f' --prop axisTitle.color=475569' + f' --prop axisTitle.size=11' + f' --prop axisTitle.bold=true' + f' --prop axisTitle.font="Helvetica Neue"' + f' --prop "axisfont=9:64748B:Helvetica Neue"' + f' --prop "axisline=CBD5E1:1"' + f' --prop gridlineColor=E2E8F0' + f' --prop dataLabels=true' + f' --prop "datalabels.numfmt=0"' + f' --prop legend=top' + f' --prop legend.overlay=false' + f' --prop "legendfont=10:475569:Helvetica Neue"' + f' --prop plotareafill=F8FAFC' + f' --prop "plotarea.border=E2E8F0:0.75"' + f' --prop chartareafill=FFFFFF' + f' --prop "chartarea.border=CBD5E1:0.75"' + f' --prop x=14 --prop y=19 --prop width=16 --prop height=22') + +# Remove blank default Sheet1 +cli(f'remove "{FILE}" /Sheet1') + +print(f"\nDone! Generated: {FILE}") +print(" 2 sheets (8 charts total)") +print(" Sheet 1: Basics & Quartile Methods (4 charts)") +print(" Sheet 2: Axes & Styling (4 charts)") diff --git a/examples/excel/charts/charts-boxwhisker.xlsx b/examples/excel/charts/charts-boxwhisker.xlsx new file mode 100644 index 000000000..3340de35d Binary files /dev/null and b/examples/excel/charts/charts-boxwhisker.xlsx differ diff --git a/examples/excel/charts/charts-bubble.md b/examples/excel/charts/charts-bubble.md new file mode 100644 index 000000000..83fd30750 --- /dev/null +++ b/examples/excel/charts/charts-bubble.md @@ -0,0 +1,119 @@ +# Bubble Charts Showcase + +This demo consists of three files that work together: + +- **charts-bubble.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-bubble.xlsx** — The generated workbook with 4 sheets (1 default + 3 chart sheets, 12 charts total). +- **charts-bubble.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-bubble.py +# -> charts-bubble.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Bubble Fundamentals + +Four bubble charts covering basic rendering, bubble scale, size representation, and data labels. + +```bash +# Basic bubble with 2 series (X,Y,Size triplets separated by semicolons) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop series1="Enterprise:50,12,80;120,8,45;200,15,60" \ + --prop series2="Consumer:30,25,50;80,18,35;150,22,70" \ + --prop catTitle=Market Size ($M) --prop axisTitle=Growth Rate (%) + +# bubbleScale=100 with center data labels +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop bubbleScale=100 \ + --prop dataLabels=true --prop labelPos=center + +# Small bubbles with bubbleScale=50 +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop bubbleScale=50 + +# Size proportional to diameter (width) instead of area +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop sizeRepresents=width +``` + +**Features:** `bubble`, X;Y;Size triplet format, `catTitle`, `axisTitle`, `bubbleScale`, `dataLabels`, `labelPos=center`, `labelFont`, `sizeRepresents=width` + +### Sheet: 2-Bubble Styling + +Four styled bubble charts with title fonts, transparency, grid styling, and shadow effects. + +```bash +# Title and legend styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop title.font=Georgia --prop title.size=16 \ + --prop title.color=1F4E79 --prop title.bold=true \ + --prop legend=right --prop legendfont=10:333333:Calibri + +# Transparent overlapping bubbles (ARGB with alpha) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop colors=804472C4,80ED7D31 \ + --prop bubbleScale=120 + +# Grid and axis line styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop gridlines=D9D9D9:0.5 --prop axisfont=9:666666 \ + --prop axisLine=333333-1 + +# Shadow and fill effects +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop plotFill=F0F4F8 --prop chartFill=FAFAFA \ + --prop series.shadow=000000-4-315-2-30 +``` + +**Features:** `title.font/size/color/bold`, `legend=right`, `legendfont`, ARGB transparency (`80RRGGBB`), `bubbleScale`, `gridlines`, `axisfont`, `axisLine`, `plotFill`, `chartFill`, `series.shadow` + +### Sheet: 3-Bubble Advanced + +Four advanced bubble charts with secondary axis, reference lines, log scale, and trendlines. + +```bash +# Secondary axis for second series +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop secondaryAxis=2 + +# Reference line (growth threshold) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop referenceLine=18:Target Growth:C00000 + +# Logarithmic scale with axis range +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop axisMin=1 --prop axisMax=50 \ + --prop logBase=10 + +# Borders and trendline +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=bubble \ + --prop chartArea.border=333333-1.5 \ + --prop plotArea.border=999999-0.75 \ + --prop trendline=linear +``` + +**Features:** `secondaryAxis`, `referenceLine`, `axisMin/Max`, `logBase`, `chartArea.border`, `plotArea.border`, `trendline=linear` + +## Inspect the Generated File + +```bash +officecli query charts-bubble.xlsx chart +officecli get charts-bubble.xlsx "/1-Bubble Fundamentals/chart[1]" +``` diff --git a/examples/excel/charts/charts-bubble.py b/examples/excel/charts/charts-bubble.py new file mode 100644 index 000000000..4e431dc1b --- /dev/null +++ b/examples/excel/charts/charts-bubble.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python3 +""" +Bubble Charts Showcase — bubble scale, size representation, and styling. + +Generates: charts-bubble.xlsx + +Usage: + python3 charts-bubble.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-bubble.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Sheet: 1-Bubble Fundamentals +# ========================================================================== +print("\n--- 1-Bubble Fundamentals ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Bubble Fundamentals"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic bubble chart with 2 series +# +# officecli add charts-bubble.xlsx "/1-Bubble Fundamentals" --type chart \ +# --prop chartType=bubble \ +# --prop title="Market Analysis" \ +# --prop series1="Enterprise:50,12,80;120,8,45;200,15,60" \ +# --prop series2="Consumer:30,25,50;80,18,35;150,22,70" \ +# --prop colors=4472C4,ED7D31 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop catTitle=Market Size --prop axisTitle=Growth Rate \ +# --prop legend=bottom +# +# Features: chartType=bubble, X;Y;Size triplets, catTitle, axisTitle +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Bubble Fundamentals" --type chart' + f' --prop chartType=bubble' + f' --prop title="Market Analysis"' + f' --prop series1=Enterprise:80,45,60' + f' --prop series2=Consumer:50,35,70' + f' --prop colors=4472C4,ED7D31' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop "catTitle=Market Size" --prop "axisTitle=Growth Rate"' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: bubbleScale=100 with dataLabels +# +# officecli add charts-bubble.xlsx "/1-Bubble Fundamentals" --type chart \ +# --prop chartType=bubble \ +# --prop title="Product Portfolio" \ +# --prop series1="Products:20,30,90;60,20,50;100,10,70;140,25,40" \ +# --prop colors=2E75B6 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop bubbleScale=100 \ +# --prop dataLabels=true --prop labelPos=center \ +# --prop labelFont=9:FFFFFF:true \ +# --prop legend=bottom +# +# Features: bubbleScale=100, dataLabels with center positioning +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Bubble Fundamentals" --type chart' + f' --prop chartType=bubble' + f' --prop title="Product Portfolio"' + f' --prop series1=Products:90,50,70,40' + f' --prop colors=2E75B6' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop bubbleScale=100' + f' --prop dataLabels=true --prop labelPos=center' + f' --prop labelFont=9:FFFFFF:true' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: bubbleScale=50 vs bubbleScale=200 comparison (small scale) +# +# officecli add charts-bubble.xlsx "/1-Bubble Fundamentals" --type chart \ +# --prop chartType=bubble \ +# --prop title="Small Bubbles (Scale 50)" \ +# --prop series1="Tech:40,15,60;90,22,80;160,10,45" \ +# --prop series2="Finance:70,18,55;130,12,70;180,20,35" \ +# --prop colors=70AD47,FFC000 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop bubbleScale=50 \ +# --prop legend=bottom +# +# Features: bubbleScale=50 (smaller bubbles) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Bubble Fundamentals" --type chart' + f' --prop chartType=bubble' + f' --prop title="Small Bubbles (Scale 50)"' + f' --prop series1=Tech:60,80,45' + f' --prop series2=Finance:55,70,35' + f' --prop colors=70AD47,FFC000' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop bubbleScale=50' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: sizeRepresents=width +# +# officecli add charts-bubble.xlsx "/1-Bubble Fundamentals" --type chart \ +# --prop chartType=bubble \ +# --prop title="Size by Width" \ +# --prop series1="Regions:35,28,70;85,15,40;140,20,55;190,30,85" \ +# --prop colors=5B9BD5 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop sizeRepresents=width \ +# --prop bubbleScale=100 \ +# --prop legend=bottom +# +# Features: sizeRepresents=width (bubble diameter proportional to value) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Bubble Fundamentals" --type chart' + f' --prop chartType=bubble' + f' --prop title="Size by Width"' + f' --prop series1=Regions:70,40,55,85' + f' --prop colors=5B9BD5' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop sizeRepresents=width' + f' --prop bubbleScale=100' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 2-Bubble Styling +# ========================================================================== +print("\n--- 2-Bubble Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Bubble Styling"') + +# -------------------------------------------------------------------------- +# Chart 1: Title styling, legend positioning +# +# officecli add charts-bubble.xlsx "/2-Bubble Styling" --type chart \ +# --prop chartType=bubble \ +# --prop title="Styled Bubble Chart" \ +# --prop series1="Segment A:45,20,65;100,15,50;160,25,80" \ +# --prop series2="Segment B:60,30,45;120,10,60;175,18,40" \ +# --prop colors=1F4E79,C55A11 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop title.font=Georgia --prop title.size=16 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop legend=right --prop legendfont=10:333333:Calibri +# +# Features: title.font/size/color/bold, legend=right, legendfont +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bubble Styling" --type chart' + f' --prop chartType=bubble' + f' --prop title="Styled Bubble Chart"' + f' --prop series1=SegmentA:65,50,80' + f' --prop series2=SegmentB:45,60,40' + f' --prop colors=1F4E79,C55A11' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop title.font=Georgia --prop title.size=16' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop legend=right --prop legendfont=10:333333:Calibri') + +# -------------------------------------------------------------------------- +# Chart 2: Series colors, transparency +# +# officecli add charts-bubble.xlsx "/2-Bubble Styling" --type chart \ +# --prop chartType=bubble \ +# --prop title="Transparent Overlapping Bubbles" \ +# --prop series1="Group X:30,25,75;70,30,60;110,15,90;150,22,50" \ +# --prop series2="Group Y:50,20,65;90,28,55;130,18,80;170,12,45" \ +# --prop colors=804472C4,80ED7D31 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop bubbleScale=120 \ +# --prop legend=bottom +# +# Features: ARGB colors with alpha (80=50% transparency) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bubble Styling" --type chart' + f' --prop chartType=bubble' + f' --prop title="Transparent Overlapping Bubbles"' + f' --prop series1=GroupX:75,60,90,50' + f' --prop series2=GroupY:65,55,80,45' + f' --prop colors=804472C4,80ED7D31' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop bubbleScale=120' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: gridlines, axisfont, axisLine +# +# officecli add charts-bubble.xlsx "/2-Bubble Styling" --type chart \ +# --prop chartType=bubble \ +# --prop title="Grid & Axis Styling" \ +# --prop series1="Division 1:25,35,55;65,20,70;115,28,45" \ +# --prop series2="Division 2:40,15,60;80,25,40;130,30,75" \ +# --prop colors=2E75B6,548235 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop gridlines=D9D9D9:0.5 \ +# --prop axisfont=9:666666 \ +# --prop axisLine=333333-1 \ +# --prop legend=bottom +# +# Features: gridlines, axisfont, axisLine +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bubble Styling" --type chart' + f' --prop chartType=bubble' + f' --prop title="Grid & Axis Styling"' + f' --prop series1=Div1:55,70,45' + f' --prop series2=Div2:60,40,75' + f' --prop colors=2E75B6,548235' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop gridlines=D9D9D9:0.5' + f' --prop axisfont=9:666666' + f' --prop axisLine=333333:1' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: plotFill, chartFill, series.shadow +# +# officecli add charts-bubble.xlsx "/2-Bubble Styling" --type chart \ +# --prop chartType=bubble \ +# --prop title="Shadow & Fill Effects" \ +# --prop series1="Portfolio:35,22,80;75,28,55;120,16,65;165,32,45" \ +# --prop colors=4472C4 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop plotFill=F0F4F8 --prop chartFill=FAFAFA \ +# --prop series.shadow=000000-4-315-2-30 \ +# --prop legend=bottom +# +# Features: plotFill, chartFill, series.shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Bubble Styling" --type chart' + f' --prop chartType=bubble' + f' --prop title="Shadow & Fill Effects"' + f' --prop series1=Portfolio:80,55,65,45' + f' --prop colors=4472C4' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop plotFill=F0F4F8 --prop chartFill=FAFAFA' + f' --prop series.shadow=000000-4-315-2-30' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 3-Bubble Advanced +# ========================================================================== +print("\n--- 3-Bubble Advanced ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Bubble Advanced"') + +# -------------------------------------------------------------------------- +# Chart 1: secondaryAxis +# +# officecli add charts-bubble.xlsx "/3-Bubble Advanced" --type chart \ +# --prop chartType=bubble \ +# --prop title="Dual-Axis Bubble" \ +# --prop series1="Domestic:70,85,60,90" \ +# --prop series2="International:45,55,80,65" \ +# --prop categories=1,2,3,4 \ +# --prop colors=4472C4,ED7D31 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop secondaryAxis=2 \ +# --prop legend=bottom +# +# Features: secondaryAxis on bubble chart +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Bubble Advanced" --type chart' + f' --prop chartType=bubble' + f' --prop title="Dual-Axis Bubble"' + f' --prop series1=Domestic:70,85,60,90' + f' --prop series2=International:45,55,80,65' + f' --prop categories=1,2,3,4' + f' --prop colors=4472C4,ED7D31' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop secondaryAxis=2' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: referenceLine +# +# officecli add charts-bubble.xlsx "/3-Bubble Advanced" --type chart \ +# --prop chartType=bubble \ +# --prop title="Growth Threshold" \ +# --prop series1="Products:60,80,45,55" \ +# --prop categories=1,2,3,4 \ +# --prop colors=70AD47 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop referenceLine=50:C00000:Target \ +# --prop bubbleScale=80 \ +# --prop legend=bottom +# +# Features: referenceLine on bubble chart +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Bubble Advanced" --type chart' + f' --prop chartType=bubble' + f' --prop title="Growth Threshold"' + f' --prop series1=Products:60,80,45,55' + f' --prop categories=1,2,3,4' + f' --prop colors=70AD47' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop "referenceLine=50:C00000:Target"' + f' --prop bubbleScale=80' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: axisMin/Max, logBase +# +# officecli add charts-bubble.xlsx "/3-Bubble Advanced" --type chart \ +# --prop chartType=bubble \ +# --prop title="Log Scale Analysis" \ +# --prop series1="Markets:5,15,50,120" \ +# --prop categories=1,2,3,4 \ +# --prop colors=2E75B6 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop axisMin=1 --prop axisMax=200 \ +# --prop logBase=10 \ +# --prop bubbleScale=80 \ +# --prop legend=bottom +# +# Features: axisMin/Max, logBase=10 (logarithmic scale) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Bubble Advanced" --type chart' + f' --prop chartType=bubble' + f' --prop title="Log Scale Analysis"' + f' --prop series1=Markets:5,15,50,120' + f' --prop categories=1,2,3,4' + f' --prop colors=2E75B6' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop axisMin=1 --prop axisMax=200' + f' --prop logBase=10' + f' --prop bubbleScale=80' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: chartArea.border, plotArea.border, trendline +# +# officecli add charts-bubble.xlsx "/3-Bubble Advanced" --type chart \ +# --prop chartType=bubble \ +# --prop title="Trend & Borders" \ +# --prop series1="Investments:20,55,95,140,180" \ +# --prop categories=1,2,3,4,5 \ +# --prop colors=4472C4 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop chartArea.border=333333:1.5 \ +# --prop plotArea.border=999999:0.75 \ +# --prop trendline=linear \ +# --prop legend=bottom +# +# Features: chartArea.border, plotArea.border, trendline=linear +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Bubble Advanced" --type chart' + f' --prop chartType=bubble' + f' --prop title="Trend & Borders"' + f' --prop series1=Investments:20,55,95,140,180' + f' --prop categories=1,2,3,4,5' + f' --prop colors=4472C4' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop chartArea.border=333333:1.5' + f' --prop plotArea.border=999999:0.75' + f' --prop trendline=linear' + f' --prop legend=bottom') + +# Remove blank default Sheet1 (all data is inline) +cli(f'remove "{FILE}" /Sheet1') + +print(f"\nDone! Generated: {FILE}") +print(" 4 sheets (3 chart sheets, 12 charts total)") diff --git a/examples/excel/charts/charts-bubble.xlsx b/examples/excel/charts/charts-bubble.xlsx new file mode 100644 index 000000000..ad402425c Binary files /dev/null and b/examples/excel/charts/charts-bubble.xlsx differ diff --git a/examples/excel/charts/charts-column.md b/examples/excel/charts/charts-column.md new file mode 100644 index 000000000..626728335 --- /dev/null +++ b/examples/excel/charts/charts-column.md @@ -0,0 +1,283 @@ +# Column Charts Showcase + +This demo consists of three files that work together: + +- **charts-column.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-column.xlsx** — The generated workbook with 8 sheets (1 data + 7 chart sheets, 28 charts total). +- **charts-column.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-column.py +# → charts-column.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Column Fundamentals + +Four basic column charts covering every data input method. + +```bash +# dataRange with axis titles and axis font +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop dataRange=Sheet1!A1:E13 \ + --prop catTitle=Month --prop axisTitle=Revenue \ + --prop axisfont=9:58626E:Arial --prop gridlines=D9D9D9:0.5:dot + +# Inline named series with gap width +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop series1="Laptops:320,280,350,310" \ + --prop series2="Phones:450,420,480,460" \ + --prop categories=Jan,Feb,Mar,Apr \ + --prop colors=2E75B6,C00000,70AD47 \ + --prop gapwidth=80 + +# Cell-range series (dotted syntax) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop series1.name=East \ + --prop series1.values=Sheet1!B2:B13 \ + --prop series1.categories=Sheet1!A2:A13 \ + --prop minorGridlines=EEEEEE:0.3:dot + +# Inline data shorthand +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop 'data=Team A:85,92,78;Team B:70,80,85' \ + --prop categories=Mon,Tue,Wed \ + --prop legend=right +``` + +**Features:** `series1=Name:v1,v2`, `series1.name`/`.values`/`.categories` (cell range), `dataRange`, `data` (shorthand), `categories`, `colors`, `catTitle`, `axisTitle`, `axisfont`, `gridlines`, `minorGridlines`, `gapwidth`, `legend` (bottom, right) + +### Sheet: 2-Column Variants + +Four charts covering all column chart type variants. + +```bash +# Stacked column with center labels and series outline +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=columnStacked \ + --prop dataLabels=center \ + --prop series.outline=FFFFFF-0.5 + +# 100% stacked column — proportional +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=columnPercentStacked \ + --prop axisNumFmt=0% + +# 3D column with perspective +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column3d \ + --prop view3d=15,20,30 --prop style=3 + +# 3D column with gap depth +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column3d \ + --prop gapDepth=200 +``` + +**Features:** `columnStacked`, `columnPercentStacked`, `column3d`, `dataLabels=center`, `series.outline`, `axisNumFmt`, `view3d` (rotX,rotY,perspective), `style` (preset 1-48), `gapDepth` + +### Sheet: 3-Column Styling + +Four charts demonstrating visual styling — title formatting, shadows, gradients, and transparency. + +```bash +# Styled title +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop title.font=Georgia --prop title.size=16 \ + --prop title.color=1F4E79 --prop title.bold=true + +# Series shadow and outline effects +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop series.shadow=000000-4-315-2-40 \ + --prop series.outline=FFFFFF-0.5 + +# Per-series gradient fills +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop 'gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90;70AD47-C5E0B4:90' + +# Transparent columns on gradient background +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop transparency=30 \ + --prop plotFill=F0F4F8-D6E4F0:90 --prop chartFill=FFFFFF \ + --prop roundedCorners=true +``` + +**Features:** `title.font`/`.size`/`.color`/`.bold`, `series.shadow` (color-blur-angle-dist-opacity), `series.outline`, `gradients` (per-series), `transparency`, `plotFill` (gradient), `chartFill`, `roundedCorners` + +### Sheet: 4-Axis & Gridlines + +Four charts demonstrating every axis and gridline configuration. + +```bash +# Custom axis scaling with axis lines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop axisMin=50 --prop axisMax=250 \ + --prop majorUnit=50 --prop minorUnit=25 \ + --prop axisLine=C00000:1.5:solid --prop catAxisLine=2E75B6:1.5:solid + +# Logarithmic scale with reversed axis +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop logBase=10 --prop axisReverse=true + +# Display units with tick marks +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop dispUnits=thousands --prop axisNumFmt=#,##0 \ + --prop majorTickMark=outside --prop minorTickMark=inside + +# Hidden axes with data table +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop gridlines=none --prop axisVisible=false \ + --prop dataTable=true --prop legend=none +``` + +**Features:** `axisMin`, `axisMax`, `majorUnit`, `minorUnit`, `axisLine`, `catAxisLine`, `logBase` (logarithmic scale), `axisReverse` (flip direction), `dispUnits` (thousands/millions), `axisNumFmt`, `majorTickMark`, `minorTickMark`, `axisVisible`, `dataTable`, `gridlines=none`, `legend=none` + +### Sheet: 5-Labels & Legend + +Four charts demonstrating data label and legend customization. + +```bash +# Data labels with number format +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop dataLabels=true --prop labelPos=outsideEnd \ + --prop labelFont=9:333333:true \ + --prop dataLabels.numFmt=#,##0 + +# Custom individual labels (hide some, highlight peak) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop dataLabels=true \ + --prop dataLabel1.delete=true --prop dataLabel2.delete=true \ + --prop point4.color=C00000 --prop dataLabel4.text=Peak! + +# Legend overlay with styled font +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop legend=right --prop legend.overlay=true \ + --prop legendfont=10:333333:Calibri --prop plotFill=F5F5F5 + +# Manual layout — plotArea, title, legend positioning +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop plotArea.x=0.12 --prop plotArea.y=0.18 \ + --prop plotArea.w=0.82 --prop plotArea.h=0.55 \ + --prop title.x=0.25 --prop title.y=0.02 \ + --prop legend.x=0.15 --prop legend.y=0.82 \ + --prop legend.w=0.7 --prop legend.h=0.12 +``` + +**Features:** `dataLabels`, `labelPos` (outsideEnd/center/insideEnd/insideBase), `labelFont`, `dataLabels.numFmt`, `dataLabel{N}.delete`, `dataLabel{N}.text`, `point{N}.color`, `legend` (right), `legend.overlay`, `legendfont`, `plotFill`, `plotArea.x/y/w/h`, `title.x/y`, `legend.x/y/w/h` + +### Sheet: 6-Effects & Advanced + +Four charts demonstrating advanced features — secondary axis, reference lines, effects, and conditional coloring. + +```bash +# Secondary axis (dual scale) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop secondaryAxis=2 \ + --prop series1="Revenue:120,180,250,310" \ + --prop series2="Growth %:50,33,39,24" + +# Reference line (target threshold) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop referenceLine=150:FF0000:1.5:dash + +# Title glow/shadow effects +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop title.glow=4472C4-8-60 \ + --prop title.shadow=000000-3-315-2-40 \ + --prop series.shadow=000000-3-315-1-30 + +# Conditional coloring with chart/plot borders +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop colorRule=0:C00000:70AD47 \ + --prop referenceLine=0:888888:1:solid \ + --prop chartArea.border=D0D0D0:1:solid \ + --prop plotArea.border=E0E0E0:0.5:dot +``` + +**Features:** `secondaryAxis` (1-based series indices), `referenceLine` (value:color:width:dash), `title.glow` (color-radius-opacity), `title.shadow` (color-blur-angle-dist-opacity), `series.shadow`, `colorRule` (threshold:belowColor:aboveColor), `chartArea.border`, `plotArea.border` + +### Sheet: 7-Bar Shape & Gap + +Four charts demonstrating column gap width, overlap, and 3D bar shapes. + +```bash +# Narrow gap (bars close together) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop gapwidth=30 + +# Wide gap with negative overlap (separated bars within group) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column \ + --prop gapwidth=200 --prop overlap=-50 + +# Cylinder shape (3D) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column3d \ + --prop shape=cylinder --prop view3d=15,20,30 + +# Cone shape (3D) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=column3d \ + --prop shape=cone --prop view3d=15,20,30 +``` + +**Features:** `gapwidth` (0-500), `overlap` (-100 to 100, negative = separated), `shape` (cylinder, cone, pyramid — 3D column shapes) + +## Complete Feature Coverage + +| Feature | Sheet | +|---------|-------| +| **Chart types:** column, columnStacked, columnPercentStacked, column3d | 1, 2 | +| **Data input:** series, dataRange, data, series.name/values/categories | 1 | +| **Colors:** colors, gradients | 1, 3 | +| **Gap & overlap:** gapwidth, overlap | 1, 7 | +| **Axis scaling:** axisMin/Max, majorUnit, minorUnit | 4 | +| **Axis features:** logBase, axisReverse, dispUnits, axisNumFmt | 2, 4 | +| **Axis lines:** axisLine, catAxisLine | 4 | +| **Axis visibility:** axisVisible | 4 | +| **Tick marks:** majorTickMark, minorTickMark | 4 | +| **Gridlines:** gridlines, minorGridlines, gridlines=none | 1, 4 | +| **Data labels:** dataLabels, labelPos, labelFont, numFmt | 2, 5 | +| **Custom labels:** dataLabel{N}.text, dataLabel{N}.delete | 5 | +| **Point color:** point{N}.color | 5 | +| **Legend:** position, legendfont, legend.overlay, legend=none | 1, 4, 5 | +| **Layout:** plotArea.x/y/w/h, title.x/y, legend.x/y/w/h | 5 | +| **Effects:** series.shadow, series.outline, transparency | 2, 3 | +| **Title styling:** font, size, color, bold, glow, shadow | 3, 6 | +| **Fills:** plotFill, chartFill (solid + gradient) | 3, 5 | +| **Borders:** chartArea.border, plotArea.border | 6 | +| **Advanced:** secondaryAxis, referenceLine, colorRule | 6 | +| **3D:** view3d, gapDepth, style, shape (cylinder/cone/pyramid) | 2, 7 | +| **Other:** dataTable, roundedCorners, catTitle, axisTitle, axisfont | 1, 3, 4 | + +## Inspect the Generated File + +```bash +officecli query charts-column.xlsx chart +officecli get charts-column.xlsx "/1-Column Fundamentals/chart[1]" +``` diff --git a/examples/excel/charts/charts-column.py b/examples/excel/charts/charts-column.py new file mode 100755 index 000000000..7d6e08e1a --- /dev/null +++ b/examples/excel/charts/charts-column.py @@ -0,0 +1,928 @@ +#!/usr/bin/env python3 +""" +Column & Bar Charts Showcase — column, columnStacked, columnPercentStacked, and column3d with all variations. + +Generates: charts-column.xlsx + +Every column chart feature officecli supports is demonstrated at least once: +gap width, overlap, bar shapes, axis scaling, gridlines, data labels, +legend positioning, reference lines, secondary axis, gradients, +transparency, shadows, manual layout, and 3D rotation. + +7 sheets, 28 charts total. + + 1-Column Fundamentals 4 charts — data input variants, axis titles, inline/cell-range/data + 2-Column Variants 4 charts — columnStacked, columnPercentStacked, column3d + 3-Column Styling 4 charts — title styling, series effects, gradients, transparency + 4-Axis & Gridlines 4 charts — axis scaling, log scale, reverse, display units + 5-Labels & Legend 4 charts — data labels, custom labels, legend layout + 6-Effects & Advanced 4 charts — secondary axis, reference line, glow/shadow, colorRule + 7-Bar Shape & Gap 4 charts — gapwidth, overlap, 3D shapes (cylinder, cone, pyramid) + +Usage: + python3 charts-column.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-column.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Source data — shared across all charts +# ========================================================================== +print("\n--- Populating source data ---") + +data_cmds = [] +for j, h in enumerate(["Month", "East", "South", "North", "West"]): + data_cmds.append({"command": "set", "path": f"/Sheet1/{'ABCDE'[j]}1", "props": {"text": h, "bold": "true"}}) + +months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"] +east = [120, 135, 148, 162, 155, 178, 195, 210, 188, 172, 165, 198] +south = [95, 108, 115, 128, 142, 155, 168, 175, 160, 148, 135, 158] +north = [88, 92, 105, 118, 125, 138, 145, 152, 140, 130, 122, 142] +west = [110, 118, 130, 145, 138, 162, 175, 190, 170, 155, 148, 180] + +for i in range(12): + r = i + 2 + for j, val in enumerate([months[i], east[i], south[i], north[i], west[i]]): + data_cmds.append({"command": "set", "path": f"/Sheet1/{'ABCDE'[j]}{r}", "props": {"text": str(val)}}) + +cli(f'batch "{FILE}" --force --commands \'{json.dumps(data_cmds)}\'') + +# ========================================================================== +# Sheet: 1-Column Fundamentals +# ========================================================================== +print("\n--- 1-Column Fundamentals ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Column Fundamentals"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic column with dataRange and axis titles +# +# officecli add charts-column.xlsx "/1-Column Fundamentals" --type chart \ +# --prop chartType=column \ +# --prop title="Monthly Sales by Region" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop catTitle=Month --prop axisTitle=Revenue \ +# --prop axisfont=9:58626E:Arial \ +# --prop gridlines=D9D9D9:0.5:dot \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 +# +# Features: chartType=column, dataRange, catTitle, axisTitle, axisfont, +# gridlines, colors +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Column Fundamentals" --type chart' + f' --prop chartType=column' + f' --prop title="Monthly Sales by Region"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop catTitle=Month --prop axisTitle=Revenue' + f' --prop axisfont=9:58626E:Arial' + f' --prop gridlines=D9D9D9:0.5:dot' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000') + +# -------------------------------------------------------------------------- +# Chart 2: Inline series with custom colors and gap width +# +# officecli add charts-column.xlsx "/1-Column Fundamentals" --type chart \ +# --prop chartType=column \ +# --prop title="Q1 Product Sales" \ +# --prop series1="Laptops:320,280,350,310" \ +# --prop series2="Phones:450,420,480,460" \ +# --prop series3="Tablets:180,160,200,190" \ +# --prop categories=Jan,Feb,Mar,Apr \ +# --prop colors=2E75B6,C00000,70AD47 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop gapwidth=80 \ +# --prop legend=bottom +# +# Features: inline series (series1=Name:v1,v2,...), colors, gapwidth, +# legend=bottom +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Column Fundamentals" --type chart' + f' --prop chartType=column' + f' --prop title="Q1 Product Sales"' + f' --prop series1="Laptops:320,280,350,310"' + f' --prop series2="Phones:450,420,480,460"' + f' --prop series3="Tablets:180,160,200,190"' + f' --prop categories=Jan,Feb,Mar,Apr' + f' --prop colors=2E75B6,C00000,70AD47' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop gapwidth=80' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Dotted syntax with cell ranges +# +# officecli add charts-column.xlsx "/1-Column Fundamentals" --type chart \ +# --prop chartType=column \ +# --prop title="East vs South (Cell Range)" \ +# --prop series1.name=East \ +# --prop series1.values=Sheet1!B2:B13 \ +# --prop series1.categories=Sheet1!A2:A13 \ +# --prop series2.name=South \ +# --prop series2.values=Sheet1!C2:C13 \ +# --prop series2.categories=Sheet1!A2:A13 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,ED7D31 \ +# --prop gridlines=D9D9D9:0.5:dot \ +# --prop minorGridlines=EEEEEE:0.3:dot +# +# Features: series.name/values/categories (cell range via dotted syntax), +# minorGridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Column Fundamentals" --type chart' + f' --prop chartType=column' + f' --prop title="East vs South (Cell Range)"' + f' --prop series1.name=East' + f' --prop series1.values=Sheet1!B2:B13' + f' --prop series1.categories=Sheet1!A2:A13' + f' --prop series2.name=South' + f' --prop series2.values=Sheet1!C2:C13' + f' --prop series2.categories=Sheet1!A2:A13' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=4472C4,ED7D31' + f' --prop gridlines=D9D9D9:0.5:dot' + f' --prop minorGridlines=EEEEEE:0.3:dot') + +# -------------------------------------------------------------------------- +# Chart 4: data= shorthand format +# +# officecli add charts-column.xlsx "/1-Column Fundamentals" --type chart \ +# --prop chartType=column \ +# --prop title="Weekly Output" \ +# --prop 'data=Team A:85,92,78,95,88;Team B:70,80,85,90,75' \ +# --prop categories=Mon,Tue,Wed,Thu,Fri \ +# --prop colors=0070C0,FF6600 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=right +# +# Features: data (inline shorthand Name:v1;Name2:v2), legend=right +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Column Fundamentals" --type chart' + f' --prop chartType=column' + f' --prop title="Weekly Output"' + f' --prop "data=Team A:85,92,78,95,88;Team B:70,80,85,90,75"' + f' --prop categories=Mon,Tue,Wed,Thu,Fri' + f' --prop colors=0070C0,FF6600' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=right') + +# ========================================================================== +# Sheet: 2-Column Variants +# ========================================================================== +print("\n--- 2-Column Variants ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Column Variants"') + +# -------------------------------------------------------------------------- +# Chart 1: Stacked column with center data labels and series outline +# +# officecli add charts-column.xlsx "/2-Column Variants" --type chart \ +# --prop chartType=columnStacked \ +# --prop title="Stacked Sales by Region" \ +# --prop dataRange=Sheet1!A1:E7 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop dataLabels=center \ +# --prop series.outline=FFFFFF-0.5 \ +# --prop legend=bottom +# +# Features: columnStacked, dataLabels=center, series.outline +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Column Variants" --type chart' + f' --prop chartType=columnStacked' + f' --prop title="Stacked Sales by Region"' + f' --prop dataRange=Sheet1!A1:E7' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop dataLabels=center' + f' --prop series.outline=FFFFFF-0.5' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: 100% stacked column with axis number format +# +# officecli add charts-column.xlsx "/2-Column Variants" --type chart \ +# --prop chartType=columnPercentStacked \ +# --prop title="Regional Contribution %" \ +# --prop dataRange=Sheet1!A1:E7 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=1F4E79,2E75B6,9DC3E6,BDD7EE \ +# --prop axisNumFmt=0% \ +# --prop legend=bottom \ +# --prop gridlines=E0E0E0:0.5:solid +# +# Features: columnPercentStacked, axisNumFmt=0%, legend=bottom +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Column Variants" --type chart' + f' --prop chartType=columnPercentStacked' + f' --prop title="Regional Contribution %"' + f' --prop dataRange=Sheet1!A1:E7' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=1F4E79,2E75B6,9DC3E6,BDD7EE' + f' --prop axisNumFmt=0%' + f' --prop legend=bottom' + f' --prop gridlines=E0E0E0:0.5:solid') + +# -------------------------------------------------------------------------- +# Chart 3: 3D column with perspective and style +# +# officecli add charts-column.xlsx "/2-Column Variants" --type chart \ +# --prop chartType=column3d \ +# --prop title="3D Regional Trends" \ +# --prop dataRange=Sheet1!A1:E7 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=15,20,30 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop chartFill=F8F8F8 \ +# --prop style=3 +# +# Features: column3d, view3d (rotX,rotY,perspective), style (preset 1-48) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Column Variants" --type chart' + f' --prop chartType=column3d' + f' --prop title="3D Regional Trends"' + f' --prop dataRange=Sheet1!A1:E7' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=15,20,30' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop chartFill=F8F8F8' + f' --prop style=3') + +# -------------------------------------------------------------------------- +# Chart 4: 3D stacked column with gap depth +# +# officecli add charts-column.xlsx "/2-Column Variants" --type chart \ +# --prop chartType=column3d \ +# --prop title="3D Stacked with Gap Depth" \ +# --prop series1="East:120,135,148,162,155,178" \ +# --prop series2="South:95,108,115,128,142,155" \ +# --prop series3="North:88,92,105,118,125,138" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=15,20,30 \ +# --prop gapDepth=200 \ +# --prop colors=2E75B6,ED7D31,70AD47 \ +# --prop legend=right +# +# Features: column3d stacked, gapDepth=200 (3D depth spacing) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Column Variants" --type chart' + f' --prop chartType=column3d' + f' --prop title="3D Stacked with Gap Depth"' + f' --prop "series1=East:120,135,148,162,155,178"' + f' --prop "series2=South:95,108,115,128,142,155"' + f' --prop "series3=North:88,92,105,118,125,138"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=15,20,30' + f' --prop gapDepth=200' + f' --prop colors=2E75B6,ED7D31,70AD47' + f' --prop legend=right') + +# ========================================================================== +# Sheet: 3-Column Styling +# ========================================================================== +print("\n--- 3-Column Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Column Styling"') + +# -------------------------------------------------------------------------- +# Chart 1: Title styling — font, size, color, bold +# +# officecli add charts-column.xlsx "/3-Column Styling" --type chart \ +# --prop chartType=column \ +# --prop title="Styled Title Demo" \ +# --prop dataRange=Sheet1!A1:E7 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop title.font=Georgia --prop title.size=16 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop legend=bottom +# +# Features: title.font=Georgia, title.size=16, title.color=1F4E79, +# title.bold=true +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Column Styling" --type chart' + f' --prop chartType=column' + f' --prop title="Styled Title Demo"' + f' --prop dataRange=Sheet1!A1:E7' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop title.font=Georgia --prop title.size=16' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: Series shadow and outline effects +# +# officecli add charts-column.xlsx "/3-Column Styling" --type chart \ +# --prop chartType=column \ +# --prop title="Shadow & Outline Effects" \ +# --prop series1="Revenue:320,280,350,310,340" \ +# --prop series2="Cost:210,195,230,220,215" \ +# --prop categories=Q1,Q2,Q3,Q4,Q5 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,C00000 \ +# --prop series.shadow=000000-4-315-2-40 \ +# --prop series.outline=FFFFFF-0.5 \ +# --prop gapwidth=100 \ +# --prop legend=bottom +# +# Features: series.shadow (color-blur-angle-dist-opacity), +# series.outline (color-width) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Column Styling" --type chart' + f' --prop chartType=column' + f' --prop title="Shadow & Outline Effects"' + f' --prop "series1=Revenue:320,280,350,310,340"' + f' --prop "series2=Cost:210,195,230,220,215"' + f' --prop categories=Q1,Q2,Q3,Q4,Q5' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=4472C4,C00000' + f' --prop series.shadow=000000-4-315-2-40' + f' --prop series.outline=FFFFFF-0.5' + f' --prop gapwidth=100' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Per-series gradient fills +# +# officecli add charts-column.xlsx "/3-Column Styling" --type chart \ +# --prop chartType=column \ +# --prop title="Gradient Columns" \ +# --prop series1="East:120,135,148,162" \ +# --prop series2="South:95,108,115,128" \ +# --prop series3="North:88,92,105,118" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop 'gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90;70AD47-C5E0B4:90' \ +# --prop legend=bottom +# +# Features: gradients (per-series gradient fills, start-end:angle) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Column Styling" --type chart' + f' --prop chartType=column' + f' --prop title="Gradient Columns"' + f' --prop "series1=East:120,135,148,162"' + f' --prop "series2=South:95,108,115,128"' + f' --prop "series3=North:88,92,105,118"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop "gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90;70AD47-C5E0B4:90"' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Transparency + plotFill gradient + chartFill + roundedCorners +# +# officecli add charts-column.xlsx "/3-Column Styling" --type chart \ +# --prop chartType=column \ +# --prop title="Transparent Columns on Gradient" \ +# --prop dataRange=Sheet1!A1:E7 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop transparency=30 \ +# --prop plotFill=F0F4F8-D6E4F0:90 \ +# --prop chartFill=FFFFFF \ +# --prop colors=1F4E79,2E75B6,5B9BD5,9DC3E6 \ +# --prop roundedCorners=true \ +# --prop legend=bottom +# +# Features: transparency=30, plotFill gradient, chartFill, roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Column Styling" --type chart' + f' --prop chartType=column' + f' --prop title="Transparent Columns on Gradient"' + f' --prop dataRange=Sheet1!A1:E7' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop transparency=30' + f' --prop plotFill=F0F4F8-D6E4F0:90' + f' --prop chartFill=FFFFFF' + f' --prop colors=1F4E79,2E75B6,5B9BD5,9DC3E6' + f' --prop roundedCorners=true' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 4-Axis & Gridlines +# ========================================================================== +print("\n--- 4-Axis & Gridlines ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Axis & Gridlines"') + +# -------------------------------------------------------------------------- +# Chart 1: Custom axis scaling — min, max, majorUnit, minorUnit +# +# officecli add charts-column.xlsx "/4-Axis & Gridlines" --type chart \ +# --prop chartType=column \ +# --prop title="Custom Axis Scale (50–250)" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop axisMin=50 --prop axisMax=250 --prop majorUnit=50 \ +# --prop minorUnit=25 \ +# --prop gridlines=D0D0D0:0.5:solid \ +# --prop minorGridlines=EEEEEE:0.3:dot \ +# --prop axisLine=C00000:1.5:solid \ +# --prop catAxisLine=2E75B6:1.5:solid \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 +# +# Features: axisMin, axisMax, majorUnit, minorUnit, +# axisLine (value axis line styling), catAxisLine (category axis line) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Gridlines" --type chart' + f' --prop chartType=column' + f' --prop title="Custom Axis Scale (50-250)"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop axisMin=50 --prop axisMax=250 --prop majorUnit=50' + f' --prop minorUnit=25' + f' --prop gridlines=D0D0D0:0.5:solid' + f' --prop minorGridlines=EEEEEE:0.3:dot' + f' --prop axisLine=C00000:1.5:solid' + f' --prop catAxisLine=2E75B6:1.5:solid' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000') + +# -------------------------------------------------------------------------- +# Chart 2: Logarithmic scale with reversed axis +# +# officecli add charts-column.xlsx "/4-Axis & Gridlines" --type chart \ +# --prop chartType=column \ +# --prop title="Log Scale (Base 10)" \ +# --prop series1="Growth:1,10,100,1000,5000" \ +# --prop categories=Year 1,Year 2,Year 3,Year 4,Year 5 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop logBase=10 \ +# --prop axisReverse=true \ +# --prop colors=C00000 \ +# --prop axisTitle="Value (log)" \ +# --prop catTitle=Year \ +# --prop gridlines=E0E0E0:0.5:dash +# +# Features: logBase=10 (logarithmic scale), axisReverse=true +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Gridlines" --type chart' + f' --prop chartType=column' + f' --prop title="Log Scale (Base 10)"' + f' --prop "series1=Growth:1,10,100,1000,5000"' + f' --prop "categories=Year 1,Year 2,Year 3,Year 4,Year 5"' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop logBase=10' + f' --prop axisReverse=true' + f' --prop colors=C00000' + f' --prop axisTitle="Value (log)"' + f' --prop catTitle=Year' + f' --prop gridlines=E0E0E0:0.5:dash') + +# -------------------------------------------------------------------------- +# Chart 3: Display units and axis number format +# +# officecli add charts-column.xlsx "/4-Axis & Gridlines" --type chart \ +# --prop chartType=column \ +# --prop title="Revenue (in Thousands)" \ +# --prop series1="Revenue:12000,18500,22000,31000,45000,52000" \ +# --prop series2="Cost:8000,11000,14000,19500,28000,33000" \ +# --prop categories=2020,2021,2022,2023,2024,2025 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop dispUnits=thousands \ +# --prop axisNumFmt=#,##0 \ +# --prop colors=2E75B6,C00000 \ +# --prop catTitle=Year --prop axisTitle=Amount (K) \ +# --prop majorTickMark=outside --prop minorTickMark=inside \ +# --prop legend=bottom +# +# Features: dispUnits=thousands, axisNumFmt=#,##0, +# majorTickMark=outside, minorTickMark=inside +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Gridlines" --type chart' + f' --prop chartType=column' + f' --prop title="Revenue (in Thousands)"' + f' --prop "series1=Revenue:12000,18500,22000,31000,45000,52000"' + f' --prop "series2=Cost:8000,11000,14000,19500,28000,33000"' + f' --prop categories=2020,2021,2022,2023,2024,2025' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop dispUnits=thousands' + f' --prop axisNumFmt=#,##0' + f' --prop colors=2E75B6,C00000' + f' --prop catTitle=Year --prop axisTitle="Amount (K)"' + f' --prop majorTickMark=outside --prop minorTickMark=inside' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Hidden axes with data table +# +# officecli add charts-column.xlsx "/4-Axis & Gridlines" --type chart \ +# --prop chartType=column \ +# --prop title="Minimal Chart with Data Table" \ +# --prop dataRange=Sheet1!A1:E7 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop gridlines=none \ +# --prop axisVisible=false \ +# --prop dataTable=true \ +# --prop legend=none \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 +# +# Features: gridlines=none, axisVisible=false, dataTable=true, legend=none +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Gridlines" --type chart' + f' --prop chartType=column' + f' --prop title="Minimal Chart with Data Table"' + f' --prop dataRange=Sheet1!A1:E7' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop gridlines=none' + f' --prop axisVisible=false' + f' --prop dataTable=true' + f' --prop legend=none' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000') + +# ========================================================================== +# Sheet: 5-Labels & Legend +# ========================================================================== +print("\n--- 5-Labels & Legend ---") +cli(f'add "{FILE}" / --type sheet --prop name="5-Labels & Legend"') + +# -------------------------------------------------------------------------- +# Chart 1: Data labels with number format and styled label font +# +# officecli add charts-column.xlsx "/5-Labels & Legend" --type chart \ +# --prop chartType=column \ +# --prop title="Sales with Labels" \ +# --prop series1="Revenue:120,180,210,250,280" \ +# --prop categories=Jan,Feb,Mar,Apr,May \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=4472C4 \ +# --prop dataLabels=true --prop labelPos=outsideEnd \ +# --prop labelFont=9:333333:true \ +# --prop dataLabels.numFmt=#,##0 +# +# Features: dataLabels=true, labelPos=outsideEnd, labelFont (size:color:bold), +# dataLabels.numFmt +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Labels & Legend" --type chart' + f' --prop chartType=column' + f' --prop title="Sales with Labels"' + f' --prop "series1=Revenue:120,180,210,250,280"' + f' --prop categories=Jan,Feb,Mar,Apr,May' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=4472C4' + f' --prop dataLabels=true --prop labelPos=outsideEnd' + f' --prop labelFont=9:333333:true' + f' --prop dataLabels.numFmt=#,##0') + +# -------------------------------------------------------------------------- +# Chart 2: Custom individual labels — delete some, highlight peak +# +# officecli add charts-column.xlsx "/5-Labels & Legend" --type chart \ +# --prop chartType=column \ +# --prop title="Peak Highlight" \ +# --prop series1="Sales:88,120,165,210,195,178" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=2E75B6 \ +# --prop dataLabels=true --prop labelPos=outsideEnd \ +# --prop dataLabel1.delete=true --prop dataLabel2.delete=true \ +# --prop dataLabel3.delete=true \ +# --prop point4.color=C00000 \ +# --prop dataLabel4.text=Peak! \ +# --prop dataLabel5.delete=true --prop dataLabel6.delete=true +# +# Features: dataLabel{N}.delete, dataLabel{N}.text, point{N}.color +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Labels & Legend" --type chart' + f' --prop chartType=column' + f' --prop title="Peak Highlight"' + f' --prop "series1=Sales:88,120,165,210,195,178"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=2E75B6' + f' --prop dataLabels=true --prop labelPos=outsideEnd' + f' --prop dataLabel1.delete=true --prop dataLabel2.delete=true' + f' --prop dataLabel3.delete=true' + f' --prop point4.color=C00000' + f' --prop dataLabel4.text=Peak!' + f' --prop dataLabel5.delete=true --prop dataLabel6.delete=true') + +# -------------------------------------------------------------------------- +# Chart 3: Legend positioning and overlay with styled legend font +# +# officecli add charts-column.xlsx "/5-Labels & Legend" --type chart \ +# --prop chartType=column \ +# --prop title="Legend Overlay on Chart" \ +# --prop dataRange=Sheet1!A1:E7 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop legend=right \ +# --prop legend.overlay=true \ +# --prop legendfont=10:333333:Calibri \ +# --prop plotFill=F5F5F5 +# +# Features: legend=right, legend.overlay=true, legendfont (size:color:fontname), +# plotFill +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Labels & Legend" --type chart' + f' --prop chartType=column' + f' --prop title="Legend Overlay on Chart"' + f' --prop dataRange=Sheet1!A1:E7' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop legend=right' + f' --prop legend.overlay=true' + f' --prop legendfont=10:333333:Calibri' + f' --prop plotFill=F5F5F5') + +# -------------------------------------------------------------------------- +# Chart 4: Manual layout — plotArea, title, and legend positioning +# +# officecli add charts-column.xlsx "/5-Labels & Legend" --type chart \ +# --prop chartType=column \ +# --prop title="Manual Layout Control" \ +# --prop dataRange=Sheet1!A1:E7 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=2E75B6,ED7D31,70AD47,FFC000 \ +# --prop plotArea.x=0.12 --prop plotArea.y=0.18 \ +# --prop plotArea.w=0.82 --prop plotArea.h=0.55 \ +# --prop title.x=0.25 --prop title.y=0.02 \ +# --prop legend.x=0.15 --prop legend.y=0.82 \ +# --prop legend.w=0.7 --prop legend.h=0.12 \ +# --prop title.font=Arial --prop title.size=13 \ +# --prop title.bold=true +# +# Features: plotArea.x/y/w/h, title.x/y, legend.x/y/w/h (manual layout) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Labels & Legend" --type chart' + f' --prop chartType=column' + f' --prop title="Manual Layout Control"' + f' --prop dataRange=Sheet1!A1:E7' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=2E75B6,ED7D31,70AD47,FFC000' + f' --prop plotArea.x=0.12 --prop plotArea.y=0.18' + f' --prop plotArea.w=0.82 --prop plotArea.h=0.55' + f' --prop title.x=0.25 --prop title.y=0.02' + f' --prop legend.x=0.15 --prop legend.y=0.82' + f' --prop legend.w=0.7 --prop legend.h=0.12' + f' --prop title.font=Arial --prop title.size=13' + f' --prop title.bold=true') + +# ========================================================================== +# Sheet: 6-Effects & Advanced +# ========================================================================== +print("\n--- 6-Effects & Advanced ---") +cli(f'add "{FILE}" / --type sheet --prop name="6-Effects & Advanced"') + +# -------------------------------------------------------------------------- +# Chart 1: Secondary axis — dual Y-axis +# +# officecli add charts-column.xlsx "/6-Effects & Advanced" --type chart \ +# --prop chartType=column \ +# --prop title="Revenue vs Growth Rate" \ +# --prop series1="Revenue:120,180,250,310,380,420" \ +# --prop series2="Growth %:50,33,39,24,23,11" \ +# --prop categories=2020,2021,2022,2023,2024,2025 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop secondaryAxis=2 \ +# --prop colors=2E75B6,C00000 \ +# --prop catTitle=Year --prop axisTitle=Revenue \ +# --prop dataLabels=true --prop labelPos=outsideEnd \ +# --prop legend=bottom +# +# Features: secondaryAxis=2 (series 2 on right-hand axis) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Effects & Advanced" --type chart' + f' --prop chartType=column' + f' --prop title="Revenue vs Growth Rate"' + f' --prop "series1=Revenue:120,180,250,310,380,420"' + f' --prop "series2=Growth %:50,33,39,24,23,11"' + f' --prop categories=2020,2021,2022,2023,2024,2025' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop secondaryAxis=2' + f' --prop colors=2E75B6,C00000' + f' --prop catTitle=Year --prop axisTitle=Revenue' + f' --prop dataLabels=true --prop labelPos=outsideEnd' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: Reference line (target/threshold) +# +# officecli add charts-column.xlsx "/6-Effects & Advanced" --type chart \ +# --prop chartType=column \ +# --prop title="vs Target (150)" \ +# --prop dataRange=Sheet1!A1:C13 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,70AD47 \ +# --prop referenceLine=150:FF0000:1.5:dash \ +# --prop legend=bottom +# +# referenceLine format: value:color:width:dash +# +# Features: referenceLine (horizontal target line) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Effects & Advanced" --type chart' + f' --prop chartType=column' + f' --prop title="vs Target (150)"' + f' --prop dataRange=Sheet1!A1:C13' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=4472C4,70AD47' + f' --prop referenceLine=150:FF0000:1.5:dash' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Title glow and shadow effects +# +# officecli add charts-column.xlsx "/6-Effects & Advanced" --type chart \ +# --prop chartType=column \ +# --prop title="Glow & Shadow Effects" \ +# --prop series1="East:120,135,148,162,155,178" \ +# --prop series2="West:110,118,130,145,138,162" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,ED7D31 \ +# --prop title.glow=4472C4-8-60 \ +# --prop title.shadow=000000-3-315-2-40 \ +# --prop title.font=Calibri --prop title.size=16 \ +# --prop title.bold=true --prop title.color=1F4E79 \ +# --prop series.shadow=000000-3-315-1-30 \ +# --prop plotFill=F0F4F8 --prop chartFill=FFFFFF +# +# Features: title.glow (color-radius-opacity), title.shadow, +# series.shadow on column charts +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Effects & Advanced" --type chart' + f' --prop chartType=column' + f' --prop title="Glow & Shadow Effects"' + f' --prop "series1=East:120,135,148,162,155,178"' + f' --prop "series2=West:110,118,130,145,138,162"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=4472C4,ED7D31' + f' --prop title.glow=4472C4-8-60' + f' --prop title.shadow=000000-3-315-2-40' + f' --prop title.font=Calibri --prop title.size=16' + f' --prop title.bold=true --prop title.color=1F4E79' + f' --prop series.shadow=000000-3-315-1-30' + f' --prop plotFill=F0F4F8 --prop chartFill=FFFFFF') + +# -------------------------------------------------------------------------- +# Chart 4: Conditional coloring with chart/plot borders +# +# officecli add charts-column.xlsx "/6-Effects & Advanced" --type chart \ +# --prop chartType=column \ +# --prop title="Profit: Conditional Colors" \ +# --prop series1="Profit:80,120,-30,160,-50,200,140,-20,180,90" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=2E75B6 \ +# --prop colorRule=0:C00000:70AD47 \ +# --prop referenceLine=0:888888:1:solid \ +# --prop chartArea.border=D0D0D0:1:solid \ +# --prop plotArea.border=E0E0E0:0.5:dot \ +# --prop dataLabels=true --prop labelPos=outsideEnd \ +# --prop labelFont=8:666666:false +# +# colorRule format: threshold:belowColor:aboveColor +# +# Features: colorRule (threshold-based conditional coloring), +# chartArea.border, plotArea.border +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Effects & Advanced" --type chart' + f' --prop chartType=column' + f' --prop title="Profit: Conditional Colors"' + f' --prop "series1=Profit:80,120,-30,160,-50,200,140,-20,180,90"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=2E75B6' + f' --prop colorRule=0:C00000:70AD47' + f' --prop referenceLine=0:888888:1:solid' + f' --prop chartArea.border=D0D0D0:1:solid' + f' --prop plotArea.border=E0E0E0:0.5:dot' + f' --prop dataLabels=true --prop labelPos=outsideEnd' + f' --prop labelFont=8:666666:false') + +# ========================================================================== +# Sheet: 7-Bar Shape & Gap +# ========================================================================== +print("\n--- 7-Bar Shape & Gap ---") +cli(f'add "{FILE}" / --type sheet --prop name="7-Bar Shape & Gap"') + +# -------------------------------------------------------------------------- +# Chart 1: Narrow gap width (bars close together) +# +# officecli add charts-column.xlsx "/7-Bar Shape & Gap" --type chart \ +# --prop chartType=column \ +# --prop title="Narrow Gap (30%)" \ +# --prop dataRange=Sheet1!A1:E7 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop gapwidth=30 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop legend=bottom +# +# Features: gapwidth=30 (narrow gaps between column groups) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Bar Shape & Gap" --type chart' + f' --prop chartType=column' + f' --prop title="Narrow Gap (30%)"' + f' --prop dataRange=Sheet1!A1:E7' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop gapwidth=30' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: Wide gap with negative overlap (separated bars within group) +# +# officecli add charts-column.xlsx "/7-Bar Shape & Gap" --type chart \ +# --prop chartType=column \ +# --prop title="Wide Gap + Negative Overlap" \ +# --prop dataRange=Sheet1!A1:E7 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop gapwidth=200 \ +# --prop overlap=-50 \ +# --prop colors=2E75B6,ED7D31,70AD47,FFC000 \ +# --prop legend=bottom +# +# Features: gapwidth=200 (wide gap), overlap=-50 (negative = bars separated) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Bar Shape & Gap" --type chart' + f' --prop chartType=column' + f' --prop title="Wide Gap + Negative Overlap"' + f' --prop dataRange=Sheet1!A1:E7' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop gapwidth=200' + f' --prop overlap=-50' + f' --prop colors=2E75B6,ED7D31,70AD47,FFC000' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: 3D column with cylinder shape +# +# officecli add charts-column.xlsx "/7-Bar Shape & Gap" --type chart \ +# --prop chartType=column3d \ +# --prop title="Cylinder Shape" \ +# --prop series1="East:120,135,148,162,155,178" \ +# --prop series2="South:95,108,115,128,142,155" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop shape=cylinder \ +# --prop view3d=15,20,30 \ +# --prop colors=4472C4,ED7D31 \ +# --prop legend=bottom +# +# Features: shape=cylinder (3D column bar shape) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Bar Shape & Gap" --type chart' + f' --prop chartType=column3d' + f' --prop title="Cylinder Shape"' + f' --prop "series1=East:120,135,148,162,155,178"' + f' --prop "series2=South:95,108,115,128,142,155"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop shape=cylinder' + f' --prop view3d=15,20,30' + f' --prop colors=4472C4,ED7D31' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: 3D column with cone/pyramid shapes +# +# officecli add charts-column.xlsx "/7-Bar Shape & Gap" --type chart \ +# --prop chartType=column3d \ +# --prop title="Cone Shape" \ +# --prop series1="North:88,92,105,118,125,138" \ +# --prop series2="West:110,118,130,145,138,162" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop shape=cone \ +# --prop view3d=15,20,30 \ +# --prop colors=70AD47,FFC000 \ +# --prop legend=bottom +# +# Features: shape=cone (3D column bar shape — also supports pyramid) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Bar Shape & Gap" --type chart' + f' --prop chartType=column3d' + f' --prop title="Cone Shape"' + f' --prop "series1=North:88,92,105,118,125,138"' + f' --prop "series2=West:110,118,130,145,138,162"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop shape=cone' + f' --prop view3d=15,20,30' + f' --prop colors=70AD47,FFC000' + f' --prop legend=bottom') + +print(f"\nDone! Generated: {FILE}") +print(" 8 sheets (Sheet1 data + 7 chart sheets, 28 charts total)") diff --git a/examples/excel/charts/charts-column.xlsx b/examples/excel/charts/charts-column.xlsx new file mode 100644 index 000000000..c6c79362f Binary files /dev/null and b/examples/excel/charts/charts-column.xlsx differ diff --git a/examples/excel/charts/charts-combo.md b/examples/excel/charts/charts-combo.md new file mode 100644 index 000000000..0b8e7bc26 --- /dev/null +++ b/examples/excel/charts/charts-combo.md @@ -0,0 +1,152 @@ +# Combo Charts Showcase + +This demo consists of three files that work together: + +- **charts-combo.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-combo.xlsx** — The generated workbook with 5 sheets (1 default + 4 chart sheets, 16 charts total). +- **charts-combo.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-combo.py +# -> charts-combo.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Combo Fundamentals + +Four combo charts covering comboSplit, secondaryAxis, combotypes, and combined usage. + +```bash +# Basic combo: 2 bar series + 1 line via comboSplit +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop series1="Revenue:120,145,160,180,195" \ + --prop series2="Expenses:90,100,110,115,125" \ + --prop series3="Margin %:25,31,31,36,36" \ + --prop comboSplit=2 --prop legend=bottom + +# Combo with secondary Y-axis for line series +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop comboSplit=1 --prop secondaryAxis=2 \ + --prop catTitle=Year --prop axisTitle=Sales ($K) + +# Per-series type control via combotypes +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop combotypes=column,column,line,area + +# combotypes + secondaryAxis together +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop combotypes=column,column,line \ + --prop secondaryAxis=3 +``` + +**Features:** `combo`, `comboSplit`, `secondaryAxis`, `combotypes=column,column,line,area`, `catTitle`, `axisTitle` + +### Sheet: 2-Combo Styling + +Four styled combo charts with title fonts, gradients, data labels, and chart fills. + +```bash +# Title, legend, axis font styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop title.font=Georgia --prop title.size=16 \ + --prop title.color=1F4E79 --prop title.bold=true \ + --prop legendfont=10:333333:Calibri --prop axisfont=9:666666 + +# Series shadow and gradients +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop 'gradients=1F4E79-5B9BD5:90;C55A11-F4B183:90' \ + --prop series.shadow=000000-4-315-2-30 + +# Data labels on combo series +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop dataLabels=true --prop labelPos=top \ + --prop labelFont=9:333333:true + +# Chart area styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop plotFill=F0F4F8 --prop chartFill=FAFAFA \ + --prop roundedCorners=true +``` + +**Features:** `title.font/size/color/bold`, `legendfont`, `axisfont`, `gradients`, `series.shadow`, `dataLabels`, `labelPos`, `labelFont`, `plotFill`, `chartFill`, `roundedCorners` + +### Sheet: 3-Combo Advanced + +Four advanced combo charts with reference lines, axis scaling, layout, and markers. + +```bash +# Reference line and gridlines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop referenceLine=110:Target:C00000 \ + --prop gridlines=D9D9D9:0.5 + +# Axis scaling and display units +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop axisMin=1000000 --prop axisMax=2000000 \ + --prop dispUnits=thousands + +# Manual plot layout +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop plotLayout=0.1,0.15,0.85,0.75 + +# Multiple line series with markers +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop comboSplit=1 --prop secondaryAxis=2,3,4 \ + --prop markers=circle-6 +``` + +**Features:** `referenceLine`, `gridlines`, `axisMin/Max`, `dispUnits`, `plotLayout`, `markers`, multiple secondary axis series + +### Sheet: 4-Combo Effects + +Four effect-heavy combo charts with glow, borders, color rules, and complex multi-series. + +```bash +# Title glow and shadow effects +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop title.glow=4472C4-6 \ + --prop title.shadow=000000-3-315-2-30 + +# Chart and plot area borders +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop chartArea.border=333333-1.5 \ + --prop plotArea.border=999999-0.75 + +# Color rule (conditional bar coloring) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop colorRule=80:C00000:70AD47 + +# 5-series dashboard with mixed combotypes +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=combo \ + --prop combotypes=column,column,column,area,line \ + --prop secondaryAxis=5 +``` + +**Features:** `title.glow`, `title.shadow`, `chartArea.border`, `plotArea.border`, `colorRule`, 5-series `combotypes` + +## Inspect the Generated File + +```bash +officecli query charts-combo.xlsx chart +officecli get charts-combo.xlsx "/1-Combo Fundamentals/chart[1]" +``` diff --git a/examples/excel/charts/charts-combo.py b/examples/excel/charts/charts-combo.py new file mode 100644 index 000000000..be6dc10e1 --- /dev/null +++ b/examples/excel/charts/charts-combo.py @@ -0,0 +1,583 @@ +#!/usr/bin/env python3 +""" +Combo Charts Showcase — column+line, column+area, secondary axes, and styling. + +Generates: charts-combo.xlsx + +Usage: + python3 charts-combo.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-combo.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Sheet: 1-Combo Fundamentals +# ========================================================================== +print("\n--- 1-Combo Fundamentals ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Combo Fundamentals"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic combo with comboSplit (2 bar series + 1 line) +# +# officecli add charts-combo.xlsx "/1-Combo Fundamentals" --type chart \ +# --prop chartType=combo \ +# --prop title="Revenue vs Expenses vs Margin" \ +# --prop series1="Revenue:120,145,160,180,195" \ +# --prop series2="Expenses:90,100,110,115,125" \ +# --prop series3="Margin %:25,31,31,36,36" \ +# --prop categories=Q1,Q2,Q3,Q4,Q5 \ +# --prop comboSplit=2 \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop legend=bottom +# +# Features: chartType=combo, comboSplit=2 (first 2 as bars, rest as lines) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Combo Fundamentals" --type chart' + f' --prop chartType=combo' + f' --prop title="Revenue vs Expenses vs Margin"' + f' --prop series1=Revenue:120,145,160,180,195' + f' --prop series2=Expenses:90,100,110,115,125' + f' --prop "series3=Margin %:25,31,31,36,36"' + f' --prop categories=Q1,Q2,Q3,Q4,Q5' + f' --prop comboSplit=2' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: Combo with secondaryAxis (line on right Y-axis) +# +# officecli add charts-combo.xlsx "/1-Combo Fundamentals" --type chart \ +# --prop chartType=combo \ +# --prop title="Sales & Growth Rate" \ +# --prop series1="Sales ($K):320,380,420,510,560" \ +# --prop series2="Growth %:8,19,11,21,10" \ +# --prop categories=2021,2022,2023,2024,2025 \ +# --prop comboSplit=1 \ +# --prop secondaryAxis=2 \ +# --prop colors=2E75B6,C00000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop legend=bottom \ +# --prop catTitle=Year --prop axisTitle=Sales ($K) +# +# Features: secondaryAxis=2 (series 2 on right Y-axis), catTitle, axisTitle +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Combo Fundamentals" --type chart' + f' --prop chartType=combo' + f' --prop title="Sales & Growth Rate"' + f' --prop "series1=Sales ($K):320,380,420,510,560"' + f' --prop "series2=Growth %:8,19,11,21,10"' + f' --prop categories=2021,2022,2023,2024,2025' + f' --prop comboSplit=1' + f' --prop secondaryAxis=2' + f' --prop colors=2E75B6,C00000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop legend=bottom' + f' --prop catTitle=Year --prop "axisTitle=Sales ($K)"') + +# -------------------------------------------------------------------------- +# Chart 3: combotypes per-series type control +# +# officecli add charts-combo.xlsx "/1-Combo Fundamentals" --type chart \ +# --prop chartType=combo \ +# --prop title="Mixed Series Types" \ +# --prop series1="Product A:50,65,70,80,90" \ +# --prop series2="Product B:40,55,60,72,85" \ +# --prop series3="Trend:48,62,68,78,88" \ +# --prop series4="Forecast:30,40,50,55,65" \ +# --prop categories=Jan,Feb,Mar,Apr,May \ +# --prop combotypes=column,column,line,area \ +# --prop colors=4472C4,ED7D31,70AD47,BDD7EE \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=bottom +# +# Features: combotypes=column,column,line,area (per-series type) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Combo Fundamentals" --type chart' + f' --prop chartType=combo' + f' --prop title="Mixed Series Types"' + f' --prop "series1=Product A:50,65,70,80,90"' + f' --prop "series2=Product B:40,55,60,72,85"' + f' --prop series3=Trend:48,62,68,78,88' + f' --prop series4=Forecast:30,40,50,55,65' + f' --prop categories=Jan,Feb,Mar,Apr,May' + f' --prop combotypes=column,column,line,area' + f' --prop colors=4472C4,ED7D31,70AD47,BDD7EE' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: combotypes with secondaryAxis +# +# officecli add charts-combo.xlsx "/1-Combo Fundamentals" --type chart \ +# --prop chartType=combo \ +# --prop title="Revenue Mix & Margin" \ +# --prop series1="Domestic:200,220,250,270,300" \ +# --prop series2="Export:80,95,110,130,150" \ +# --prop series3="Net Margin %:18,20,22,24,26" \ +# --prop categories=2021,2022,2023,2024,2025 \ +# --prop combotypes=column,column,line \ +# --prop secondaryAxis=3 \ +# --prop colors=4472C4,9DC3E6,C00000 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=bottom \ +# --prop catTitle=Year +# +# Features: combotypes + secondaryAxis together +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Combo Fundamentals" --type chart' + f' --prop chartType=combo' + f' --prop title="Revenue Mix & Margin"' + f' --prop series1=Domestic:200,220,250,270,300' + f' --prop series2=Export:80,95,110,130,150' + f' --prop "series3=Net Margin %:18,20,22,24,26"' + f' --prop categories=2021,2022,2023,2024,2025' + f' --prop combotypes=column,column,line' + f' --prop secondaryAxis=3' + f' --prop colors=4472C4,9DC3E6,C00000' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=bottom' + f' --prop catTitle=Year') + +# ========================================================================== +# Sheet: 2-Combo Styling +# ========================================================================== +print("\n--- 2-Combo Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Combo Styling"') + +# -------------------------------------------------------------------------- +# Chart 1: Title, legend, axisfont styling +# +# officecli add charts-combo.xlsx "/2-Combo Styling" --type chart \ +# --prop chartType=combo \ +# --prop title="Styled Combo Chart" \ +# --prop series1="Revenue:150,175,200,220" \ +# --prop series2="COGS:100,110,130,140" \ +# --prop series3="Profit %:33,37,35,36" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop comboSplit=2 \ +# --prop colors=1F4E79,5B9BD5,70AD47 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop title.font=Georgia --prop title.size=16 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop legend=bottom --prop legendfont=10:333333:Calibri \ +# --prop axisfont=9:666666 +# +# Features: title.font/size/color/bold, legendfont, axisfont +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Combo Styling" --type chart' + f' --prop chartType=combo' + f' --prop title="Styled Combo Chart"' + f' --prop series1=Revenue:150,175,200,220' + f' --prop series2=COGS:100,110,130,140' + f' --prop "series3=Profit %:33,37,35,36"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop comboSplit=2' + f' --prop colors=1F4E79,5B9BD5,70AD47' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop title.font=Georgia --prop title.size=16' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop legend=bottom --prop legendfont=10:333333:Calibri' + f' --prop axisfont=9:666666') + +# -------------------------------------------------------------------------- +# Chart 2: Series shadow, gradients +# +# officecli add charts-combo.xlsx "/2-Combo Styling" --type chart \ +# --prop chartType=combo \ +# --prop title="Gradient & Shadow Effects" \ +# --prop series1="Actual:85,92,105,120,135" \ +# --prop series2="Budget:80,90,100,110,120" \ +# --prop series3="Variance:5,2,5,10,15" \ +# --prop categories=Jan,Feb,Mar,Apr,May \ +# --prop comboSplit=2 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop 'gradients=1F4E79-5B9BD5:90;C55A11-F4B183:90' \ +# --prop series.shadow=000000-4-315-2-30 \ +# --prop legend=bottom +# +# Features: gradients (per-bar-series), series.shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Combo Styling" --type chart' + f' --prop chartType=combo' + f' --prop title="Gradient & Shadow Effects"' + f' --prop series1=Actual:85,92,105,120,135' + f' --prop series2=Budget:80,90,100,110,120' + f' --prop series3=Variance:5,2,5,10,15' + f' --prop categories=Jan,Feb,Mar,Apr,May' + f' --prop comboSplit=2' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop "gradients=1F4E79-5B9BD5:90;C55A11-F4B183:90"' + f' --prop series.shadow=000000-4-315-2-30' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: dataLabels on line series +# +# officecli add charts-combo.xlsx "/2-Combo Styling" --type chart \ +# --prop chartType=combo \ +# --prop title="Data Labels on Lines" \ +# --prop series1="Units:500,620,710,800" \ +# --prop series2="Avg Price:45,48,52,55" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop comboSplit=1 \ +# --prop secondaryAxis=2 \ +# --prop colors=4472C4,ED7D31 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=top \ +# --prop labelFont=9:333333:true \ +# --prop legend=bottom +# +# Features: dataLabels=true, labelPos=top, labelFont +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Combo Styling" --type chart' + f' --prop chartType=combo' + f' --prop title="Data Labels on Lines"' + f' --prop series1=Units:500,620,710,800' + f' --prop "series2=Avg Price:45,48,52,55"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop comboSplit=1' + f' --prop secondaryAxis=2' + f' --prop colors=4472C4,ED7D31' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelPos=top' + f' --prop labelFont=9:333333:true' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: plotFill, chartFill, roundedCorners +# +# officecli add charts-combo.xlsx "/2-Combo Styling" --type chart \ +# --prop chartType=combo \ +# --prop title="Chart Area Styling" \ +# --prop series1="Online:180,210,240,260,290" \ +# --prop series2="Retail:150,140,135,130,120" \ +# --prop series3="Growth %:5,12,15,10,12" \ +# --prop categories=2021,2022,2023,2024,2025 \ +# --prop comboSplit=2 \ +# --prop colors=2E75B6,ED7D31,70AD47 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop plotFill=F0F4F8 --prop chartFill=FAFAFA \ +# --prop roundedCorners=true \ +# --prop legend=bottom +# +# Features: plotFill, chartFill, roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Combo Styling" --type chart' + f' --prop chartType=combo' + f' --prop title="Chart Area Styling"' + f' --prop series1=Online:180,210,240,260,290' + f' --prop series2=Retail:150,140,135,130,120' + f' --prop "series3=Growth %:5,12,15,10,12"' + f' --prop categories=2021,2022,2023,2024,2025' + f' --prop comboSplit=2' + f' --prop colors=2E75B6,ED7D31,70AD47' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop plotFill=F0F4F8 --prop chartFill=FAFAFA' + f' --prop roundedCorners=true' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 3-Combo Advanced +# ========================================================================== +print("\n--- 3-Combo Advanced ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Combo Advanced"') + +# -------------------------------------------------------------------------- +# Chart 1: referenceLine, gridlines +# +# officecli add charts-combo.xlsx "/3-Combo Advanced" --type chart \ +# --prop chartType=combo \ +# --prop title="Target Reference Line" \ +# --prop series1="Actual:95,105,115,125,130" \ +# --prop series2="Forecast:90,100,110,120,130" \ +# --prop categories=Jan,Feb,Mar,Apr,May \ +# --prop comboSplit=1 \ +# --prop colors=4472C4,BDD7EE \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop referenceLine=110:C00000:Target \ +# --prop gridlines=D9D9D9:0.5 \ +# --prop legend=bottom +# +# Features: referenceLine=value:label:color, gridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Combo Advanced" --type chart' + f' --prop chartType=combo' + f' --prop title="Target Reference Line"' + f' --prop series1=Actual:95,105,115,125,130' + f' --prop series2=Forecast:90,100,110,120,130' + f' --prop categories=Jan,Feb,Mar,Apr,May' + f' --prop comboSplit=1' + f' --prop colors=4472C4,BDD7EE' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop referenceLine=110:C00000:Target' + f' --prop gridlines=D9D9D9:0.5' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: axisMin/Max, dispUnits +# +# officecli add charts-combo.xlsx "/3-Combo Advanced" --type chart \ +# --prop chartType=combo \ +# --prop title="Axis Scaling & Units" \ +# --prop series1="Revenue:1200000,1450000,1600000,1800000" \ +# --prop series2="Profit %:18,22,25,28" \ +# --prop categories=2022,2023,2024,2025 \ +# --prop comboSplit=1 \ +# --prop secondaryAxis=2 \ +# --prop colors=2E75B6,70AD47 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop axisMin=1000000 --prop axisMax=2000000 \ +# --prop dispUnits=thousands \ +# --prop legend=bottom +# +# Features: axisMin/Max, dispUnits=thousands +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Combo Advanced" --type chart' + f' --prop chartType=combo' + f' --prop title="Axis Scaling & Units"' + f' --prop series1=Revenue:1200000,1450000,1600000,1800000' + f' --prop "series2=Profit %:18,22,25,28"' + f' --prop categories=2022,2023,2024,2025' + f' --prop comboSplit=1' + f' --prop secondaryAxis=2' + f' --prop colors=2E75B6,70AD47' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop axisMin=1000000 --prop axisMax=2000000' + f' --prop dispUnits=thousands' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Manual layout +# +# officecli add charts-combo.xlsx "/3-Combo Advanced" --type chart \ +# --prop chartType=combo \ +# --prop title="Manual Layout" \ +# --prop series1="Plan:100,120,140,160" \ +# --prop series2="Actual:95,125,135,170" \ +# --prop series3="Delta %:-5,4,-4,6" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop comboSplit=2 \ +# --prop secondaryAxis=3 \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop plotLayout=0.1,0.15,0.85,0.75 \ +# --prop legend=bottom +# +# Features: plotLayout=left,top,width,height (manual plot area) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Combo Advanced" --type chart' + f' --prop chartType=combo' + f' --prop title="Manual Layout"' + f' --prop series1=Plan:100,120,140,160' + f' --prop series2=Actual:95,125,135,170' + f' --prop "series3=Delta %:-5,4,-4,6"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop comboSplit=2' + f' --prop secondaryAxis=3' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop plotLayout=0.1,0.15,0.85,0.75' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Multiple line series with markers + bar series +# +# officecli add charts-combo.xlsx "/3-Combo Advanced" --type chart \ +# --prop chartType=combo \ +# --prop title="Multi-Line with Markers" \ +# --prop series1="Units Sold:800,920,1050,1200,1350" \ +# --prop series2="North:30,35,38,42,45" \ +# --prop series3="South:25,28,32,36,40" \ +# --prop series4="West:20,24,28,32,35" \ +# --prop categories=Q1,Q2,Q3,Q4,Q5 \ +# --prop comboSplit=1 \ +# --prop secondaryAxis=2,3,4 \ +# --prop colors=4472C4,C00000,70AD47,FFC000 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop markers=circle-6 \ +# --prop legend=bottom +# +# Features: multiple line series on secondary axis, markers +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Combo Advanced" --type chart' + f' --prop chartType=combo' + f' --prop title="Multi-Line with Markers"' + f' --prop "series1=Units Sold:800,920,1050,1200,1350"' + f' --prop series2=North:30,35,38,42,45' + f' --prop series3=South:25,28,32,36,40' + f' --prop series4=West:20,24,28,32,35' + f' --prop categories=Q1,Q2,Q3,Q4,Q5' + f' --prop comboSplit=1' + f' --prop secondaryAxis=2,3,4' + f' --prop colors=4472C4,C00000,70AD47,FFC000' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop markers=circle-6' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 4-Combo Effects +# ========================================================================== +print("\n--- 4-Combo Effects ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Combo Effects"') + +# -------------------------------------------------------------------------- +# Chart 1: title.glow, title.shadow +# +# officecli add charts-combo.xlsx "/4-Combo Effects" --type chart \ +# --prop chartType=combo \ +# --prop title="Glowing Title" \ +# --prop series1="Metric A:60,72,85,90,100" \ +# --prop series2="Metric B:40,50,55,62,70" \ +# --prop series3="Ratio:67,69,65,69,70" \ +# --prop categories=W1,W2,W3,W4,W5 \ +# --prop comboSplit=2 \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop title.glow=4472C4-6 \ +# --prop title.shadow=000000-3-315-2-30 \ +# --prop legend=bottom +# +# Features: title.glow=color-radius, title.shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Combo Effects" --type chart' + f' --prop chartType=combo' + f' --prop title="Glowing Title"' + f' --prop "series1=Metric A:60,72,85,90,100"' + f' --prop "series2=Metric B:40,50,55,62,70"' + f' --prop series3=Ratio:67,69,65,69,70' + f' --prop categories=W1,W2,W3,W4,W5' + f' --prop comboSplit=2' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop title.glow=4472C4-6' + f' --prop title.shadow=000000-3-315-2-30' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: chartArea.border, plotArea.border +# +# officecli add charts-combo.xlsx "/4-Combo Effects" --type chart \ +# --prop chartType=combo \ +# --prop title="Bordered Areas" \ +# --prop series1="Income:250,280,310,340" \ +# --prop series2="Costs:180,195,210,225" \ +# --prop series3="Margin %:28,30,32,34" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop comboSplit=2 \ +# --prop colors=2E75B6,ED7D31,548235 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop chartArea.border=333333:1.5 \ +# --prop plotArea.border=999999:0.75 \ +# --prop legend=bottom +# +# Features: chartArea.border=color-width, plotArea.border +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Combo Effects" --type chart' + f' --prop chartType=combo' + f' --prop title="Bordered Areas"' + f' --prop series1=Income:250,280,310,340' + f' --prop series2=Costs:180,195,210,225' + f' --prop "series3=Margin %:28,30,32,34"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop comboSplit=2' + f' --prop colors=2E75B6,ED7D31,548235' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop chartArea.border=333333:1.5' + f' --prop plotArea.border=999999:0.75' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: colorRule +# +# officecli add charts-combo.xlsx "/4-Combo Effects" --type chart \ +# --prop chartType=combo \ +# --prop title="Color Rule Combo" \ +# --prop series1="Performance:72,85,65,90,78" \ +# --prop series2="Target:80,80,80,80,80" \ +# --prop categories=Team A,Team B,Team C,Team D,Team E \ +# --prop comboSplit=1 \ +# --prop colors=4472C4,C00000 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colorRule=80:C00000:70AD47 \ +# --prop legend=bottom +# +# Features: colorRule=threshold:belowColor:aboveColor +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Combo Effects" --type chart' + f' --prop chartType=combo' + f' --prop title="Color Rule Combo"' + f' --prop series1=Performance:72,85,65,90,78' + f' --prop series2=Target:80,80,80,80,80' + f' --prop "categories=Team A,Team B,Team C,Team D,Team E"' + f' --prop comboSplit=1' + f' --prop colors=4472C4,C00000' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop colorRule=80:C00000:70AD47' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Complex combo with 5+ series +# +# officecli add charts-combo.xlsx "/4-Combo Effects" --type chart \ +# --prop chartType=combo \ +# --prop title="Full Business Dashboard" \ +# --prop series1="Revenue:500,550,600,650,700" \ +# --prop series2="COGS:300,320,340,360,380" \ +# --prop series3="OpEx:100,105,110,115,120" \ +# --prop series4="Net Income:100,125,150,175,200" \ +# --prop series5="Margin %:20,23,25,27,29" \ +# --prop categories=2021,2022,2023,2024,2025 \ +# --prop combotypes=column,column,column,area,line \ +# --prop secondaryAxis=5 \ +# --prop colors=4472C4,ED7D31,A5A5A5,BDD7EE,C00000 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=bottom \ +# --prop gridlines=E0E0E0:0.5 +# +# Features: 5 series, mixed combotypes, secondary axis +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Combo Effects" --type chart' + f' --prop chartType=combo' + f' --prop title="Full Business Dashboard"' + f' --prop series1=Revenue:500,550,600,650,700' + f' --prop series2=COGS:300,320,340,360,380' + f' --prop series3=OpEx:100,105,110,115,120' + f' --prop "series4=Net Income:100,125,150,175,200"' + f' --prop "series5=Margin %:20,23,25,27,29"' + f' --prop categories=2021,2022,2023,2024,2025' + f' --prop combotypes=column,column,column,area,line' + f' --prop secondaryAxis=5' + f' --prop colors=4472C4,ED7D31,A5A5A5,BDD7EE,C00000' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=bottom' + f' --prop gridlines=E0E0E0:0.5') + +# Remove blank default Sheet1 (all data is inline) +cli(f'remove "{FILE}" /Sheet1') + +print(f"\nDone! Generated: {FILE}") +print(" 5 sheets (4 chart sheets, 16 charts total)") diff --git a/examples/excel/charts/charts-combo.xlsx b/examples/excel/charts/charts-combo.xlsx new file mode 100644 index 000000000..4f56e30b8 Binary files /dev/null and b/examples/excel/charts/charts-combo.xlsx differ diff --git a/examples/excel/charts/charts-demo.md b/examples/excel/charts/charts-demo.md new file mode 100644 index 000000000..09fb5b1d2 --- /dev/null +++ b/examples/excel/charts/charts-demo.md @@ -0,0 +1,6 @@ +# charts-demo + +TODO: rewrite script with high-level chart API, add annotated officecli commands. + +See [charts-demo.sh](charts-demo.sh) and [charts-demo.xlsx](charts-demo.xlsx). + diff --git a/examples/excel/gen-charts-demo.sh b/examples/excel/charts/charts-demo.sh similarity index 99% rename from examples/excel/gen-charts-demo.sh rename to examples/excel/charts/charts-demo.sh index 1bcd8fae0..53447525c 100755 --- a/examples/excel/gen-charts-demo.sh +++ b/examples/excel/charts/charts-demo.sh @@ -5,7 +5,7 @@ set -e -XLSX="charts_demo.xlsx" +XLSX="$(dirname "$0")/charts-demo.xlsx" echo "" echo "==========================================" echo "Generating Excel chart showcase: $XLSX" diff --git a/examples/excel/charts/charts-demo.xlsx b/examples/excel/charts/charts-demo.xlsx new file mode 100644 index 000000000..5d6dc3aff Binary files /dev/null and b/examples/excel/charts/charts-demo.xlsx differ diff --git a/examples/excel/charts/charts-extended.md b/examples/excel/charts/charts-extended.md new file mode 100644 index 000000000..926623c21 --- /dev/null +++ b/examples/excel/charts/charts-extended.md @@ -0,0 +1,282 @@ +# Extended Chart Types Showcase + +This demo consists of three files that work together: + +- **charts-extended.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-extended.xlsx** — The generated workbook: 3 sheets, 14 charts, covering every property supported by the cx:chart family (waterfall, funnel, treemap, sunburst, histogram, boxWhisker). +- **charts-extended.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-extended.py +# → charts-extended.xlsx +``` + +## Feature Coverage Summary + +Every extended-chart-specific knob is exercised by at least one chart: + +| Chart type | Specific knobs | Covered by | +|---|---|---| +| waterfall | `increaseColor`, `decreaseColor`, `totalColor`, `chartFill`, `labelFont` | Sheet 1, Chart 1–2 | +| funnel | (generic styling only) | Sheet 1, Chart 3–4 | +| pareto | auto-sort desc, `ownerIdx` cumulative-% line, secondary % axis | Sheet 4, Chart 1–2 | +| treemap | `parentLabelLayout` = `overlapping` / `banner` / `none` | Sheet 2, Chart 1/2/3 | +| sunburst | (generic styling only) | Sheet 2, Chart 4 | +| histogram | `binCount`, `binSize`, `intervalClosed` = `r` / `l`, `underflowBin`, `overflowBin` | Sheet 3, Chart 1–4 | +| boxWhisker | `quartileMethod` = `exclusive` / `inclusive` | Sheet 3, Chart 5–6 | + +Generic cx styling exercised across the deck: `title.glow`, `title.shadow`, `title.bold`/`size`/`color`, `dataLabels`, `labelFont`, `legend` position, `legendfont`, `axisfont`, `colors` palette, `chartFill`, `plotFill`. + +> **Notes on cx:chart limitations:** +> +> - `chartFill` / `plotFill` only accept a **solid** hex color (or `none`). Unlike regular cChart, gradient `C1-C2:angle` is not supported. +> - `colors=` palette **does not work per-data-point** on single-series cx charts (funnel, treemap, sunburst). OfficeCLI only applies the first palette color to the whole series, so every bar/tile/segment ends up the same color. Omit `colors=` on these charts and let Excel's theme drive the default rainbow. `colors=` still works normally on multi-series cx charts (boxWhisker) and on all regular cChart types. + +--- + +## Sheet: 1-Waterfall & Funnel + +Two waterfall charts (financial bridges) and two funnel charts (pipelines). + +```bash +# Chart 1 — waterfall with increase/decrease/total colors + data labels + title glow +officecli add charts-extended.xlsx "/1-Waterfall & Funnel" --type chart \ + --prop chartType=waterfall \ + --prop title="Cash Flow Bridge" \ + --prop data="Start:1000,Revenue:500,Costs:-300,Tax:-100,Net:1100" \ + --prop increaseColor=70AD47 --prop decreaseColor=FF0000 --prop totalColor=4472C4 \ + --prop dataLabels=true \ + --prop title.glow="00D2FF-6-60" + +# Chart 2 — waterfall with legend + chartFill (solid) + custom label font +officecli add charts-extended.xlsx "/1-Waterfall & Funnel" --type chart \ + --prop chartType=waterfall \ + --prop title="Budget vs Actual" \ + --prop data="Budget:5000,Sales:2000,Marketing:-800,Ops:-600,Net:5600" \ + --prop increaseColor=2E75B6 --prop decreaseColor=C00000 --prop totalColor=FFC000 \ + --prop legend=bottom \ + --prop chartFill=F0F4FA \ + --prop dataLabels=true \ + --prop labelFont="9:333333:true" + +# Chart 3 — funnel (sales pipeline) with title shadow +officecli add charts-extended.xlsx "/1-Waterfall & Funnel" --type chart \ + --prop chartType=funnel \ + --prop title="Sales Pipeline" \ + --prop series1="Pipeline:1200,850,600,300,120" \ + --prop categories=Leads,Qualified,Proposal,Negotiation,Won \ + --prop dataLabels=true \ + --prop title.shadow="000000-4-45-2-40" + +# Chart 4 — funnel (marketing) with custom colors palette, legend/axis fonts +officecli add charts-extended.xlsx "/1-Waterfall & Funnel" --type chart \ + --prop chartType=funnel \ + --prop title="Marketing Funnel" \ + --prop series1="Users:10000,6500,3200,1800,900,450" \ + --prop categories=Impressions,Clicks,Signups,Active,Paying,Retained \ + --prop dataLabels=true \ + --prop legendfont="9:8B949E:Helvetica Neue" \ + --prop axisfont="10:58626E:Helvetica Neue" +``` + +**Features:** `chartType=waterfall`, `increaseColor`, `decreaseColor`, `totalColor`, `chartType=funnel`, descending pipeline values, `dataLabels`, `title.glow`, `title.shadow`, `legend=bottom`, `chartFill` (solid hex), `labelFont`, `colors` palette, `legendfont`, `axisfont`. + +--- + +## Sheet: 2-Treemap & Sunburst + +Three treemaps (one per `parentLabelLayout` value) and one sunburst. + +```bash +# Chart 1 — treemap with parentLabelLayout=overlapping + dataLabels +officecli add charts-extended.xlsx "/2-Treemap & Sunburst" --type chart \ + --prop chartType=treemap \ + --prop title="Revenue by Product" \ + --prop series1="Revenue:450,380,310,280,210,180,150,120" \ + --prop categories=Laptops,Phones,Tablets,TVs,Cameras,Audio,Gaming,Wearables \ + --prop parentLabelLayout=overlapping \ + --prop dataLabels=true + +# Chart 2 — treemap with parentLabelLayout=banner + title styling +officecli add charts-extended.xlsx "/2-Treemap & Sunburst" --type chart \ + --prop chartType=treemap \ + --prop title="Department Budget" \ + --prop series1="Budget:900,750,600,500,420,350,280" \ + --prop categories=Engineering,Sales,Marketing,Support,Finance,HR,Legal \ + --prop parentLabelLayout=banner \ + --prop title.bold=true --prop title.size=14 --prop title.color=2E5090 + +# Chart 3 — treemap with parentLabelLayout=none (flat, no parent header strip) +officecli add charts-extended.xlsx "/2-Treemap & Sunburst" --type chart \ + --prop chartType=treemap \ + --prop title="Flat Treemap (no parent labels)" \ + --prop series1="Units:250,200,180,160,140,120,100,80,60,40" \ + --prop categories=A,B,C,D,E,F,G,H,I,J \ + --prop parentLabelLayout=none \ + --prop dataLabels=true + +# Chart 4 — sunburst with chartFill + plotFill (solid) + colors palette +officecli add charts-extended.xlsx "/2-Treemap & Sunburst" --type chart \ + --prop chartType=sunburst \ + --prop title="Market Share by Region" \ + --prop series1="Share:35,25,20,15,30,25,20,10,15" \ + --prop categories=North,South,East,West,Urban,Suburban,Rural,Online,Retail \ + --prop chartFill=F8FAFC --prop plotFill=FFFFFF \ + --prop dataLabels=true +``` + +**Features:** `chartType=treemap`, `parentLabelLayout=overlapping`, `parentLabelLayout=banner`, `parentLabelLayout=none`, `chartType=sunburst`, radial hierarchical layout, `colors` palette, `title.bold`/`size`/`color`, `dataLabels`, `chartFill` + `plotFill` (solid). + +--- + +## Sheet: 3-Histogram & BoxWhisker + +Four histograms covering every binning knob, and two box-and-whisker charts (one per quartile method). + +```bash +# Chart 1 — histogram with auto-binning (no binCount/binSize) +officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ + --prop chartType=histogram \ + --prop title="Test Scores (auto bins)" \ + --prop series1="Scores:45,52,58,61,63,...,95,97,99" + +# Chart 2 — histogram with explicit binCount=5 + title glow +officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ + --prop chartType=histogram \ + --prop title="Sales (binCount=5)" \ + --prop series1="Sales:120,135,...,620,700" \ + --prop binCount=5 \ + --prop title.glow="FFC000-6-50" + +# Chart 3 — histogram with explicit binSize=50 (fixed bin width) + label font +officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ + --prop chartType=histogram \ + --prop title="Sales (binSize=50)" \ + --prop series1="Sales:120,135,...,620,700" \ + --prop binSize=50 \ + --prop dataLabels=true --prop labelFont="9:FFFFFF:true" + +# Chart 4 — histogram with underflowBin + overflowBin + intervalClosed=l +officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ + --prop chartType=histogram \ + --prop title="Response Time (outlier bins)" \ + --prop series1="ms:40,55,68,75,...,220,280,350" \ + --prop underflowBin=60 \ + --prop overflowBin=200 \ + --prop intervalClosed=l \ + --prop dataLabels=true \ + --prop legend=none + +# Chart 5 — box & whisker, two teams, quartileMethod=exclusive +officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ + --prop chartType=boxWhisker \ + --prop title="Response Time by Team (ms)" \ + --prop series1="TeamA:42,55,...,105,120" \ + --prop series2="TeamB:30,38,...,92,110" \ + --prop quartileMethod=exclusive \ + --prop legend=bottom + +# Chart 6 — box & whisker, three departments, quartileMethod=inclusive + title glow +officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ + --prop chartType=boxWhisker \ + --prop title="Salary Distribution (\$k)" \ + --prop series1="Engineering:85,92,...,150,180" \ + --prop series2="Marketing:60,65,...,98,110" \ + --prop series3="Sales:55,62,...,160,190" \ + --prop quartileMethod=inclusive \ + --prop title.glow="00D2FF-6-60" \ + --prop legend=bottom +``` + +**Features:** `chartType=histogram`, auto-binning, `binCount` (explicit count), `binSize` (explicit width — mutually exclusive with `binCount`), `underflowBin` (cutoff for `N`), `intervalClosed=r` (default, `(a,b]`) vs `intervalClosed=l` (`[a,b)`), `chartType=boxWhisker`, `quartileMethod=exclusive`, `quartileMethod=inclusive`, multi-series grouping (2 or 3), `title.glow`, `legend=bottom`, `legend=none`, `labelFont`, `dataLabels`. + +--- + +## Sheet: 4-Pareto + +Two Pareto charts demonstrating automatic descending sort and cumulative-% overlay line. + +```bash +# Chart 1 — categorical Pareto (defect analysis), pre-sorted input +officecli add charts-extended.xlsx "/4-Pareto" --type chart \ + --prop chartType=pareto \ + --prop title="Defect Pareto" \ + --prop series1="Count:45,30,10,8,5,2" \ + --prop categories=Scratches,Dents,Cracks,Chips,Stains,Other \ + --prop dataLabels=true + +# Chart 2 — Pareto with out-of-order input (auto-sorted desc by officecli) +officecli add charts-extended.xlsx "/4-Pareto" --type chart \ + --prop chartType=pareto \ + --prop title="Root Cause Pareto" \ + --prop series1="Tickets:12,87,5,45,3,120,22,67,8,31" \ + --prop categories=Network,Auth,DB,Cache,UI,Config,Deploy,Monitor,Queue,Storage \ + --prop title.glow="FFC000-6-50" \ + --prop legend=bottom +``` + +**Features:** `chartType=pareto`, automatic descending sort of values + categories, cumulative-% overlay line on secondary 0-100% axis (auto-generated via `ownerIdx`), `dataLabels`, `title.glow`, `legend=bottom`. Input is a SINGLE user series; officecli synthesizes the 2-series structure internally (clusteredColumn bars + paretoLine with `ownerIdx="0"` + secondary percentage axis). + +--- + +## Property Reference + +| Property | Applies to | Example value | Sheet | +|---|---|---|---| +| `chartType=waterfall` | waterfall | `waterfall` | 1 | +| `chartType=funnel` | funnel | `funnel` | 1 | +| `chartType=treemap` | treemap | `treemap` | 2 | +| `chartType=sunburst` | sunburst | `sunburst` | 2 | +| `chartType=histogram` | histogram | `histogram` | 3 | +| `chartType=boxWhisker` | boxWhisker | `boxWhisker` | 3 | +| `chartType=pareto` | pareto | `pareto` | 4 | +| `data=` name:value pairs | waterfall | `Start:1000,Revenue:500,...` | 1 | +| `increaseColor` | waterfall | `70AD47` | 1 | +| `decreaseColor` | waterfall | `FF0000` | 1 | +| `totalColor` | waterfall | `4472C4` | 1 | +| `series1=Name:values`, `series2=...`, `series3=...` | all cx | `TeamA:42,55,...` | 1/2/3 | +| `categories` | all cx except histogram | `Leads,Qualified,...` | 1/2 | +| `parentLabelLayout` | treemap | `overlapping` \| `banner` \| `none` | 2 | +| `binCount` | histogram | `5` | 3 | +| `binSize` | histogram | `50` | 3 | +| `intervalClosed` | histogram | `r` (default) \| `l` | 3 | +| `underflowBin` | histogram | `60` | 3 | +| `overflowBin` | histogram | `200` | 3 | +| `quartileMethod` | boxWhisker | `exclusive` \| `inclusive` | 3 | +| `dataLabels` | all cx | `true` | 1/2/3 | +| `labelFont` | all cx | `"9:FFFFFF:true"` | 1/3 | +| `title.glow` | all cx | `"00D2FF-6-60"` | 1/3 | +| `title.shadow` | all cx | `"000000-4-45-2-40"` | 1 | +| `title.bold`/`size`/`color` | all cx | `true` / `14` / `2E5090` | 2 | +| `legend` | all cx | `bottom` \| `none` | 1/3 | +| `legendfont` | all cx | `"9:8B949E:Helvetica Neue"` | 1 | +| `axisfont` | all cx | `"10:58626E:Helvetica Neue"` | 1 | +| `colors` | multi-series cx only (not useful on funnel/treemap/sunburst — see limitations note) | `4472C4,5B9BD5,...` | — | +| `chartFill` (solid only) | all cx | `F8FAFC` | 1/2 | +| `plotFill` (solid only) | all cx | `FFFFFF` | 2 | + +--- + +## Known Validation Warning + +`officecli validate charts-extended.xlsx` reports schema warnings on histogram charts' `binCount` / `binSize` elements: + +``` +[Schema] The element '...:binCount' has invalid value ''. The text value cannot be empty. +[Schema] The 'val' attribute is not declared. +``` + +This is expected. The Open XML SDK's generated schema models `cx:binCount` as a text-valued leaf (`5`), but **real Excel writes and requires** the attribute form (``). OfficeCLI writes the Excel-compatible form via a raw unknown element; the SDK validator then complains. See `ChartExBuilder.cs:793–801` for the rationale. Files open and render correctly in Excel. + +--- + +## Inspect the Generated File + +```bash +officecli query charts-extended.xlsx chart +officecli get charts-extended.xlsx "/1-Waterfall & Funnel/chart[1]" +officecli view charts-extended.xlsx outline +``` diff --git a/examples/excel/charts/charts-extended.py b/examples/excel/charts/charts-extended.py new file mode 100644 index 000000000..73f133adf --- /dev/null +++ b/examples/excel/charts/charts-extended.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python3 +""" +Extended Chart Types Showcase — full feature coverage for waterfall, funnel, +treemap, sunburst, histogram, boxWhisker (cx:chart family). + +Covers every extended-chart-specific property plus representative generic +cx styling knobs (title.glow, chartFill gradient, legendfont, dataLabels...). + +Generates: charts-extended.xlsx + +Usage: + python3 charts-extended.py +""" + +import subprocess, sys, os, atexit + +FILE = "charts-extended.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Sheet 1: Waterfall & Funnel +# ========================================================================== +print("\n--- 1-Waterfall & Funnel ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Waterfall & Funnel"') + +# -------------------------------------------------------------------------- +# Chart 1: Waterfall — increase/decrease/total colors + data labels + title glow +# +# officecli add charts-extended.xlsx "/1-Waterfall & Funnel" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Cash Flow Bridge" \ +# --prop data="Start:1000,Revenue:500,Costs:-300,Tax:-100,Net:1100" \ +# --prop increaseColor=70AD47 \ +# --prop decreaseColor=FF0000 \ +# --prop totalColor=4472C4 \ +# --prop dataLabels=true \ +# --prop title.glow="00D2FF-6-60" \ +# --prop x=0 --prop y=0 --prop width=13 --prop height=18 +# +# Features: chartType=waterfall, increaseColor, decreaseColor, totalColor, +# dataLabels, title.glow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Waterfall & Funnel" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Cash Flow Bridge"' + f' --prop data=Start:1000,Revenue:500,Costs:-300,Tax:-100,Net:1100' + f' --prop increaseColor=70AD47' + f' --prop decreaseColor=FF0000' + f' --prop totalColor=4472C4' + f' --prop dataLabels=true' + f' --prop title.glow=00D2FF-6-60' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 2: Waterfall — chart-area gradient fill + legend + custom label font +# +# officecli add charts-extended.xlsx "/1-Waterfall & Funnel" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Budget vs Actual" \ +# --prop data="Budget:5000,Sales:2000,Marketing:-800,Ops:-600,Net:5600" \ +# --prop increaseColor=2E75B6 \ +# --prop decreaseColor=C00000 \ +# --prop totalColor=FFC000 \ +# --prop legend=bottom \ +# --prop chartFill=F0F4FA \ +# --prop dataLabels=true \ +# --prop labelFont="9:333333:true" +# +# Features: waterfall with legend=bottom, chartFill (solid hex — cx charts +# don't support gradient fills, use plain RGB), labelFont "size:color:bold" +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Waterfall & Funnel" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Budget vs Actual"' + f' --prop data=Budget:5000,Sales:2000,Marketing:-800,Ops:-600,Net:5600' + f' --prop increaseColor=2E75B6' + f' --prop decreaseColor=C00000' + f' --prop totalColor=FFC000' + f' --prop legend=bottom' + f' --prop chartFill=F0F4FA' + f' --prop dataLabels=true' + f' --prop labelFont=9:333333:true' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 3: Funnel — sales pipeline with title shadow +# +# officecli add charts-extended.xlsx "/1-Waterfall & Funnel" --type chart \ +# --prop chartType=funnel \ +# --prop title="Sales Pipeline" \ +# --prop series1="Pipeline:1200,850,600,300,120" \ +# --prop categories=Leads,Qualified,Proposal,Negotiation,Won \ +# --prop dataLabels=true \ +# --prop title.shadow="000000-4-45-2-40" +# +# Features: chartType=funnel, descending pipeline values, dataLabels, +# title.shadow "COLOR-BLUR-ANGLE-DIST-OPACITY" +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Waterfall & Funnel" --type chart' + f' --prop chartType=funnel' + f' --prop title="Sales Pipeline"' + f' --prop series1=Pipeline:1200,850,600,300,120' + f' --prop categories=Leads,Qualified,Proposal,Negotiation,Won' + f' --prop dataLabels=true' + f' --prop title.shadow=000000-4-45-2-40' + f' --prop x=0 --prop y=19 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 4: Funnel — marketing conversion + legend/axis fonts + axis titles +# +# officecli add charts-extended.xlsx "/1-Waterfall & Funnel" --type chart \ +# --prop chartType=funnel \ +# --prop title="Marketing Funnel" \ +# --prop series1="Users:10000,6500,3200,1800,900,450" \ +# --prop categories=Impressions,Clicks,Signups,Active,Paying,Retained \ +# --prop dataLabels=true \ +# --prop legendfont="9:8B949E:Helvetica Neue" \ +# --prop axisfont="10:58626E:Helvetica Neue" +# +# Features: funnel, legendfont "size:color:fontname", axisfont, +# 6-stage pipeline, dataLabels +# +# NOTE: `colors=` palette is intentionally omitted here. On cx:chart single- +# series types (funnel/treemap/sunburst) the CLI only applies the first +# palette color to the whole series, so all bars would render the same +# color. Let Excel's theme pick the default accent color. +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Waterfall & Funnel" --type chart' + f' --prop chartType=funnel' + f' --prop title="Marketing Funnel"' + f' --prop series1=Users:10000,6500,3200,1800,900,450' + f' --prop categories=Impressions,Clicks,Signups,Active,Paying,Retained' + f' --prop dataLabels=true' + f' --prop "legendfont=9:8B949E:Helvetica Neue"' + f' --prop "axisfont=10:58626E:Helvetica Neue"' + f' --prop x=14 --prop y=19 --prop width=13 --prop height=18') + +# ========================================================================== +# Sheet 2: Treemap & Sunburst +# ========================================================================== +print("\n--- 2-Treemap & Sunburst ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Treemap & Sunburst"') + +# -------------------------------------------------------------------------- +# Chart 1: Treemap — parentLabelLayout=overlapping + dataLabels +# +# officecli add charts-extended.xlsx "/2-Treemap & Sunburst" --type chart \ +# --prop chartType=treemap \ +# --prop title="Revenue by Product" \ +# --prop series1="Revenue:450,380,310,280,210,180,150,120" \ +# --prop categories=Laptops,Phones,Tablets,TVs,Cameras,Audio,Gaming,Wearables \ +# --prop parentLabelLayout=overlapping \ +# --prop dataLabels=true +# +# Features: chartType=treemap, parentLabelLayout=overlapping, dataLabels. +# NOTE: `colors=` is omitted — see Funnel Chart 4 note: cx single-series +# charts only pick up the first palette color. Excel's theme will auto- +# rainbow the tiles instead. +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Treemap & Sunburst" --type chart' + f' --prop chartType=treemap' + f' --prop title="Revenue by Product"' + f' --prop series1=Revenue:450,380,310,280,210,180,150,120' + f' --prop categories=Laptops,Phones,Tablets,TVs,Cameras,Audio,Gaming,Wearables' + f' --prop parentLabelLayout=overlapping' + f' --prop dataLabels=true' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 2: Treemap — parentLabelLayout=banner + bold title +# +# officecli add charts-extended.xlsx "/2-Treemap & Sunburst" --type chart \ +# --prop chartType=treemap \ +# --prop title="Department Budget" \ +# --prop series1="Budget:900,750,600,500,420,350,280" \ +# --prop categories=Engineering,Sales,Marketing,Support,Finance,HR,Legal \ +# --prop parentLabelLayout=banner \ +# --prop title.bold=true \ +# --prop title.size=14 \ +# --prop title.color=2E5090 +# +# Features: treemap parentLabelLayout=banner, title.bold/size/color +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Treemap & Sunburst" --type chart' + f' --prop chartType=treemap' + f' --prop title="Department Budget"' + f' --prop series1=Budget:900,750,600,500,420,350,280' + f' --prop categories=Engineering,Sales,Marketing,Support,Finance,HR,Legal' + f' --prop parentLabelLayout=banner' + f' --prop title.bold=true' + f' --prop title.size=14' + f' --prop title.color=2E5090' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 3: Treemap — parentLabelLayout=none (no parent label strip) +# +# officecli add charts-extended.xlsx "/2-Treemap & Sunburst" --type chart \ +# --prop chartType=treemap \ +# --prop title="Flat Treemap (no parent labels)" \ +# --prop series1="Units:250,200,180,160,140,120,100,80,60,40" \ +# --prop categories=A,B,C,D,E,F,G,H,I,J \ +# --prop parentLabelLayout=none \ +# --prop dataLabels=true +# +# Features: treemap parentLabelLayout=none (all labels inline, no header strip), +# dataLabels on leaf tiles +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Treemap & Sunburst" --type chart' + f' --prop chartType=treemap' + f' --prop title="Flat Treemap (no parent labels)"' + f' --prop series1=Units:250,200,180,160,140,120,100,80,60,40' + f' --prop categories=A,B,C,D,E,F,G,H,I,J' + f' --prop parentLabelLayout=none' + f' --prop dataLabels=true' + f' --prop x=0 --prop y=19 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 4: Sunburst — radial hierarchy + chartFill (solid) + plotFill +# +# officecli add charts-extended.xlsx "/2-Treemap & Sunburst" --type chart \ +# --prop chartType=sunburst \ +# --prop title="Market Share by Region" \ +# --prop series1="Share:35,25,20,15,30,25,20,10,15" \ +# --prop categories=North,South,East,West,Urban,Suburban,Rural,Online,Retail \ +# --prop chartFill=F8FAFC \ +# --prop plotFill=FFFFFF \ +# --prop dataLabels=true +# +# Features: chartType=sunburst, radial hierarchical layout, chartFill (solid hex), +# plotFill (solid hex), dataLabels. +# NOTE 1: cx:chart's chart/plot fill only accepts solid color — not gradient +# (unlike regular cChart). Use a single hex like "F8FAFC" or "none". +# NOTE 2: `colors=` palette is omitted for the same reason as the funnel/ +# treemap examples — cx single-series charts paint only the first palette +# entry. Let Excel's theme drive per-segment coloring. +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Treemap & Sunburst" --type chart' + f' --prop chartType=sunburst' + f' --prop title="Market Share by Region"' + f' --prop series1=Share:35,25,20,15,30,25,20,10,15' + f' --prop categories=North,South,East,West,Urban,Suburban,Rural,Online,Retail' + f' --prop chartFill=F8FAFC' + f' --prop plotFill=FFFFFF' + f' --prop dataLabels=true' + f' --prop x=14 --prop y=19 --prop width=13 --prop height=18') + +# ========================================================================== +# Sheet 3: Histogram & Box Whisker +# ========================================================================== +print("\n--- 3-Histogram & BoxWhisker ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Histogram & BoxWhisker"') + +# -------------------------------------------------------------------------- +# Chart 1: Histogram — auto-binning (Excel picks bin count) +# +# officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ +# --prop chartType=histogram \ +# --prop title="Test Scores (auto bins)" \ +# --prop series1="Scores:45,52,58,61,63,65,67,68,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,97,99" +# +# Features: chartType=histogram, no binning knobs → Excel auto-selects bins +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Histogram & BoxWhisker" --type chart' + f' --prop chartType=histogram' + f' --prop title="Test Scores (auto bins)"' + f' --prop series1=Scores:45,52,58,61,63,65,67,68,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,97,99' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 2: Histogram — explicit binCount=5 with title glow +# +# officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ +# --prop chartType=histogram \ +# --prop title="Sales (binCount=5)" \ +# --prop series1="Sales:120,135,148,155,162,170,175,183,191,200,210,220,235,250,265,280,295,310,340,380,420,480,550,620,700" \ +# --prop binCount=5 \ +# --prop title.glow="FFC000-6-50" +# +# Features: histogram binCount (explicit bin count), title.glow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Histogram & BoxWhisker" --type chart' + f' --prop chartType=histogram' + f' --prop title="Sales (binCount=5)"' + f' --prop series1=Sales:120,135,148,155,162,170,175,183,191,200,210,220,235,250,265,280,295,310,340,380,420,480,550,620,700' + f' --prop binCount=5' + f' --prop title.glow=FFC000-6-50' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 3: Histogram — explicit binSize=50 (fixed bin width) + label font +# +# officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ +# --prop chartType=histogram \ +# --prop title="Sales (binSize=50)" \ +# --prop series1="Sales:120,135,148,155,162,170,175,183,191,200,210,220,235,250,265,280,295,310,340,380,420,480,550,620,700" \ +# --prop binSize=50 \ +# --prop dataLabels=true \ +# --prop labelFont="9:FFFFFF:true" +# +# Features: histogram binSize (explicit bin width — mutually exclusive with +# binCount), dataLabels, labelFont +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Histogram & BoxWhisker" --type chart' + f' --prop chartType=histogram' + f' --prop title="Sales (binSize=50)"' + f' --prop series1=Sales:120,135,148,155,162,170,175,183,191,200,210,220,235,250,265,280,295,310,340,380,420,480,550,620,700' + f' --prop binSize=50' + f' --prop dataLabels=true' + f' --prop labelFont=9:FFFFFF:true' + f' --prop x=28 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 4: Histogram — overflow/underflow bins + intervalClosed=l +# +# officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ +# --prop chartType=histogram \ +# --prop title="Response Time (outlier bins)" \ +# --prop series1="ms:40,55,68,75,82,88,95,102,110,118,125,135,150,175,220,280,350" \ +# --prop underflowBin=60 \ +# --prop overflowBin=200 \ +# --prop intervalClosed=l \ +# --prop dataLabels=true \ +# --prop legend=none +# +# Features: histogram underflowBin (cutoff for N), +# intervalClosed=l (bins are [a,b) — left-closed; default "r" is (a,b]), +# legend=none +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Histogram & BoxWhisker" --type chart' + f' --prop chartType=histogram' + f' --prop title="Response Time (outlier bins)"' + f' --prop series1=ms:40,55,68,75,82,88,95,102,110,118,125,135,150,175,220,280,350' + f' --prop underflowBin=60' + f' --prop overflowBin=200' + f' --prop intervalClosed=l' + f' --prop dataLabels=true' + f' --prop legend=none' + f' --prop x=0 --prop y=19 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 5: Box & Whisker — two teams, quartileMethod=exclusive +# +# officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ +# --prop chartType=boxWhisker \ +# --prop title="Response Time by Team (ms)" \ +# --prop series1="TeamA:42,55,61,68,72,75,78,81,85,88,92,97,105,120" \ +# --prop series2="TeamB:30,38,45,52,58,62,65,68,71,74,78,85,92,110" \ +# --prop quartileMethod=exclusive \ +# --prop legend=bottom +# +# Features: chartType=boxWhisker, two-series comparison, +# quartileMethod=exclusive, legend=bottom, outlier detection (built-in) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Histogram & BoxWhisker" --type chart' + f' --prop chartType=boxWhisker' + f' --prop title="Response Time by Team (ms)"' + f' --prop "series1=TeamA:42,55,61,68,72,75,78,81,85,88,92,97,105,120"' + f' --prop "series2=TeamB:30,38,45,52,58,62,65,68,71,74,78,85,92,110"' + f' --prop quartileMethod=exclusive' + f' --prop legend=bottom' + f' --prop x=14 --prop y=19 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 6: Box & Whisker — three departments, quartileMethod=inclusive + title glow +# +# officecli add charts-extended.xlsx "/3-Histogram & BoxWhisker" --type chart \ +# --prop chartType=boxWhisker \ +# --prop title="Salary Distribution ($k)" \ +# --prop series1="Engineering:85,92,95,98,102,105,108,112,118,125,135,150,180" \ +# --prop series2="Marketing:60,65,68,72,75,78,80,83,88,92,98,110" \ +# --prop series3="Sales:55,62,68,75,82,90,98,105,115,125,140,160,190" \ +# --prop quartileMethod=inclusive \ +# --prop title.glow="00D2FF-6-60" \ +# --prop legend=bottom +# +# Features: boxWhisker three-series, quartileMethod=inclusive (different +# quartile formula from exclusive), title.glow, mean markers (default on) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Histogram & BoxWhisker" --type chart' + f' --prop chartType=boxWhisker' + f' --prop title="Salary Distribution (\\$k)"' + f' --prop "series1=Engineering:85,92,95,98,102,105,108,112,118,125,135,150,180"' + f' --prop "series2=Marketing:60,65,68,72,75,78,80,83,88,92,98,110"' + f' --prop "series3=Sales:55,62,68,75,82,90,98,105,115,125,140,160,190"' + f' --prop quartileMethod=inclusive' + f' --prop title.glow=00D2FF-6-60' + f' --prop legend=bottom' + f' --prop x=28 --prop y=19 --prop width=13 --prop height=18') + +# ========================================================================== +# Sheet 4: Pareto +# ========================================================================== +print("\n--- 4-Pareto ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Pareto"') + +# -------------------------------------------------------------------------- +# Chart 1: Pareto — defect analysis, raw counts auto-sorted + cumul% overlay +# +# officecli add charts-extended.xlsx "/4-Pareto" --type chart \ +# --prop chartType=pareto \ +# --prop title="Defect Pareto" \ +# --prop series1="Count:45,30,10,8,5,2" \ +# --prop categories=Scratches,Dents,Cracks,Chips,Stains,Other \ +# --prop dataLabels=true +# +# Features: chartType=pareto (2-series under the hood — clusteredColumn bars +# + paretoLine cumulative %), automatic descending sort, cumulative % +# computed server-side, dataLabels on both series. +# Input is a SINGLE user series; officecli pre-sorts by value desc and +# emits the two cx:series MSO expects (layoutId=clusteredColumn + +# layoutId=paretoLine with cx:binning intervalClosed="r"). +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Pareto" --type chart' + f' --prop chartType=pareto' + f' --prop title="Defect Pareto"' + f' --prop series1=Count:45,30,10,8,5,2' + f' --prop categories=Scratches,Dents,Cracks,Chips,Stains,Other' + f' --prop dataLabels=true' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# -------------------------------------------------------------------------- +# Chart 2: Pareto — root cause analysis, 10 categories, out-of-order input +# +# officecli add charts-extended.xlsx "/4-Pareto" --type chart \ +# --prop chartType=pareto \ +# --prop title="Root Cause Pareto" \ +# --prop series1="Tickets:12,87,5,45,3,120,22,67,8,31" \ +# --prop categories=Network,Auth,DB,Cache,UI,Config,Deploy,Monitor,Queue,Storage \ +# --prop title.glow="FFC000-6-50" \ +# --prop legend=bottom +# +# Features: pareto with unsorted input values (12, 87, 5, ...) — officecli +# re-sorts by value desc (120, 87, 67, ...) and re-aligns categories so +# the biggest contributor renders first. title.glow + legend=bottom +# demonstrate generic cx styling on pareto. +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Pareto" --type chart' + f' --prop chartType=pareto' + f' --prop title="Root Cause Pareto"' + f' --prop series1=Tickets:12,87,5,45,3,120,22,67,8,31' + f' --prop categories=Network,Auth,DB,Cache,UI,Config,Deploy,Monitor,Queue,Storage' + f' --prop title.glow=FFC000-6-50' + f' --prop legend=bottom' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# Remove blank default Sheet1 (all data is inline) +cli(f'remove "{FILE}" /Sheet1') + +print(f"\nDone! Generated: {FILE}") +print(" 4 sheets, 16 charts total (full cx:chart feature coverage)") +print(" Sheet 1: Waterfall (2) + Funnel (2)") +print(" Sheet 2: Treemap (3: overlapping/banner/none) + Sunburst (1)") +print(" Sheet 3: Histogram (4: auto/binCount/binSize/overflow+underflow+intervalClosed=l) + BoxWhisker (2: exclusive/inclusive)") +print(" Sheet 4: Pareto (2: sorted input / out-of-order input)") diff --git a/examples/excel/charts/charts-extended.xlsx b/examples/excel/charts/charts-extended.xlsx new file mode 100644 index 000000000..d4586aa61 Binary files /dev/null and b/examples/excel/charts/charts-extended.xlsx differ diff --git a/examples/excel/charts/charts-histogram.md b/examples/excel/charts/charts-histogram.md new file mode 100644 index 000000000..d30045dea --- /dev/null +++ b/examples/excel/charts/charts-histogram.md @@ -0,0 +1,278 @@ +# Histogram Charts — Grand Showcase + +The most thorough histogram demo officecli can produce. Every binning knob, +every styling vocabulary, every canonical distribution shape, six design +themes, four font-family type specimens, and a cohesive production-grade +ML dashboard. + +This demo is three files that work together: + +- **charts-histogram.py** — Python script that calls `officecli` to generate + the workbook. Each chart command is shown as a copyable shell command in + the comments. +- **charts-histogram.xlsx** — The generated workbook: 6 sheets, 29 charts. +- **charts-histogram.md** — This file. Maps each sheet to the features it + demonstrates and lists the full histogram property vocabulary. + +## Regenerate + +```bash +cd examples/excel +python3 charts-histogram.py +# → charts-histogram.xlsx +``` + +## Why a dedicated histogram showcase? + +Histograms are Excel's cx-namespace "extended" chart type. The binning layer +(`layoutPr/binning`) is where all the interesting knobs live — auto vs +explicit count, bin width, interval-closed side, outlier cut-offs — and +getting them right takes some care because Excel rejects the file entirely +if the XML uses the wrong form of `cx:binCount` / `cx:binSize`. + +Beyond binning, the cx pipeline in officecli has full parity with regular +cChart for typography, axis scaling, area fills/borders, drop shadows, +data labels, and legend styling. This file exercises every binning knob +AND every styling knob in one place, so you can copy-paste from whichever +row most matches the shape you want. + +## Sheets at a glance + +| Sheet | Charts | What it demonstrates | +|---|---|---| +| 0-Hero | 1 | Full-bleed magazine-grade poster using EVERY knob | +| 1-Binning Lab | 6 | Every binning strategy on one dataset, identical styling | +| 2-Distribution Zoo | 6 | Six canonical real-world distribution shapes | +| 3-Theme Gallery | 6 | Six complete design themes on the SAME dataset | +| 4-Typography | 4 | Four font-family type specimens | +| 5-ML Dashboard | 6 | Cohesive "Production ML Model Report" dashboard | + +## Sheet 0: 0-Hero + +One full-bleed 27×38-cell hero chart that combines EVERY histogram knob +into a single presentation-grade poster. Dark "Midnight Academia" palette +— navy plot area, gold bars, cream title, soft grid lines, locked Y axis, +dropped shadows on both title and series, data labels with number format, +top legend with compound font styling. If this chart renders correctly, +the entire histogram pipeline is healthy. + +```bash +officecli add charts-histogram.xlsx "/0-Hero" --type chart \ + --prop chartType=histogram \ + --prop title="The Shape of Data · 200-sample bell curve" \ + --prop title.color=F5F1E0 --prop title.size=22 --prop title.bold=true \ + --prop title.font="Helvetica Neue" \ + --prop "title.shadow=000000-8-45-4-70" \ + --prop series1="Samples:<200 bell values>" \ + --prop binCount=24 --prop intervalClosed=l \ + --prop fill=F0C96A --prop "series.shadow=000000-8-45-4-60" \ + --prop axismin=0 --prop axismax=28 --prop majorunit=4 \ + --prop xAxisTitle="Score" --prop yAxisTitle="Frequency" \ + --prop axisTitle.color=C9B87A --prop axisTitle.size=13 \ + --prop axisTitle.bold=true --prop axisTitle.font="Helvetica Neue" \ + --prop "axisfont=10:B8B090:Helvetica Neue" \ + --prop "axisline=6A6448:1.5" \ + --prop gridlineColor=2F3544 \ + --prop plotareafill=1A1F2C --prop "plotarea.border=3A3E4E:1.25" \ + --prop chartareafill=0B0F18 --prop "chartarea.border=2A2E3E:1" \ + --prop dataLabels=true --prop "datalabels.numfmt=0" \ + --prop legend=top --prop legend.overlay=false \ + --prop "legendfont=11:D4C994:Helvetica Neue" \ + --prop x=0 --prop y=0 --prop width=27 --prop height=38 +``` + +**Features:** title.color / title.size / title.bold / title.font / title.shadow, +fill, series.shadow, binCount, intervalClosed, axismin/axismax/majorunit, +xAxisTitle / yAxisTitle, axisTitle.color / axisTitle.size / axisTitle.bold / +axisTitle.font, axisfont compound, axisline, gridlineColor, plotareafill, +plotarea.border, chartareafill, chartarea.border, dataLabels, datalabels.numfmt, +legend, legend.overlay, legendfont. + +## Sheet 1: 1-Binning Lab + +Six charts, SAME dataset (200 bell-curve samples), IDENTICAL typography and +frame — the ONLY thing that varies is the binning strategy. Put side by +side, this sheet is the binning Rosetta stone. + +```bash +# 1. Auto-binning (no binCount, no binSize — Excel picks it) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=histogram --prop series1="Samples:" \ + --prop title="1 · Auto-binning (Excel default)" --prop fill=4472C4 + +# 2. Explicit binCount=8 (coarse) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=histogram --prop series1="Samples:" \ + --prop binCount=8 --prop title="2 · binCount=8 (coarse)" + +# 3. Explicit binCount=32 (fine) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=histogram --prop series1="Samples:" \ + --prop binCount=32 --prop title="3 · binCount=32 (fine)" + +# 4. Fixed bin width (binSize=5) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=histogram --prop series1="Samples:" \ + --prop binSize=5 --prop title="4 · binSize=5 (fixed-width bins)" + +# 5. Outlier fencing (underflowBin=55, overflowBin=95) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=histogram --prop series1="Samples:" \ + --prop binSize=5 --prop underflowBin=55 --prop overflowBin=95 + +# 6. Left-closed intervals [a,b) with gapWidth=30 between bars +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=histogram --prop series1="Samples:" \ + --prop binCount=16 --prop intervalClosed=l --prop gapWidth=30 +``` + +**Features:** `chartType=histogram`, auto-binning (default), `binCount=N`, +`binSize=W`, `underflowBin=N`, `overflowBin=M`, `intervalClosed=l`, `gapWidth=N` + +Notes: +- If both `binCount` and `binSize` are given, `binCount` wins. +- Histograms default `gapWidth=0` (bars touch) to match Excel's native output. +- `intervalClosed=l` makes bins half-open `[a,b)` instead of the default `(a,b]`. +- `underflow` / `overflow` fences let the interesting bulk stay readable + when the tail is catastrophic. + +## Sheet 2: 2-Distribution Zoo + +A 2×3 visual gallery of canonical real-world distribution shapes. Pattern +recognition: if you ever see one of these shapes in a telemetry chart, you +know immediately what's going on. Every chart shares the same typography +and frame; only the fill color, data, and binning strategy change. + +| Shape | Data | Fill | Binning | +|---|---|---|---| +| Normal · bell curve | 200 gauss(75, 12) | #2F5597 | binCount=18 | +| Bimodal · two cohorts | 80 gauss(55,6) + 80 gauss(88,5) | #ED7D31 | binCount=22 | +| Right-skewed · log-normal | 180 exp(gauss(3.2, 0.55)) | #70AD47 | binCount=20 | +| Left-skewed · retirement | 140 75 − exp(gauss(1.6, 0.6)) | #7030A0 | binCount=18 | +| Uniform · flat floor | 160 uniform(0, 100) | #00B0F0 | binSize=10 | +| Heavy-tailed · Pareto | 200 paretovariate(1.6) × 20 | #C00000 | binSize=20, overflow=250 | + +## Sheet 3: 3-Theme Gallery + +Six complete design themes applied to the SAME bell-curve dataset. Each +theme is a coordinated palette: plot-area fill, chart-area fill, series +fill, gridline color, axis line color, tick-label color, title color, +title font — all chosen to read as one coherent mood. + +| Theme | Mood | Plot BG | Bar | Title font | +|---|---|---|---|---| +| Midnight Academia | Dark, elegant | navy #1A1F2C | gold #F0C96A | Georgia | +| Sunset Terracotta | Warm, editorial | cream #FFF5E8 | coral #E85D4A | Georgia | +| Forest Parchment | Organic, retro | beige #F3EDD8 | forest #2F5D3A | Georgia | +| Editorial Mono | Pure grayscale | white #FFFFFF | dark #2A2A2A | Helvetica Neue | +| Neon Terminal | Cyberpunk | black #0A0A14 | cyan #00F0C8 | Courier New | +| Pastel Bloom | Soft, feminine | lavender #FDF4F8 | rose #F5A7C8 | Helvetica Neue | + +Each chart uses the full parity-knob vocabulary: `plotareafill`, +`plotarea.border`, `chartareafill`, `chartarea.border`, `gridlineColor`, +`axisline`, `axisfont`, `title.color` / `title.font`, `axisTitle.color` / +`axisTitle.font`. This is the sheet to copy-paste from when you want to +build a specific look for a report. + +## Sheet 4: 4-Typography + +Four font-family type specimens. Same data, same geometry, nearly identical +color — only the font family varies. Side by side, this sheet shows how +typography alone can reshape a chart's tone. + +| Font | Tone | Used for | +|---|---|---| +| Helvetica Neue | Modern sans | Dashboards, corporate reports | +| Georgia | Editorial serif | Magazines, long-form reports | +| Courier New | Data mono | Telemetry, engineering, terminals | +| Verdana | Friendly sans | Onboarding, public-facing UI | + +Each specimen sets `title.font`, `axisTitle.font`, and the fontname segment +of the `axisfont` compound form to the same family, so the entire chart +lives in one typographic voice. + +## Sheet 5: 5-ML Dashboard + +A cohesive "Production ML Model Report" dashboard. Every chart wears the +same uniform — typography, frames, gridlines, axis line — but each shows +a different slice of the model's behavior, deliberately using a different +color, binning strategy, and (where relevant) outlier-fencing or axis +locking. The six read as one dashboard. + +| Panel | Data shape | Color | Binning / parity knob | +|---|---|---|---| +| Inference Latency · p50–p99 | heavy-tail | #EF4444 | binSize=25, overflowBin=300, series.shadow | +| Prediction Confidence | right-skewed | #10B981 | binSize=5, axismin=0, majorunit=50 | +| Residual magnitude | half-normal | #F59E0B | binSize=0.25, intervalClosed=l | +| Token length | bimodal | #6366F1 | binCount=24 | +| GPU utilization | normal (clipped) | #8B5CF6 | binSize=5, axismin=0 axismax=50 majorunit=10 | +| Cost per request | log-normal | #EC4899 | binSize=5, overflowBin=120, dataLabels+numfmt | + +This sheet shows that one typographic uniform plus per-panel color and +binning choices is enough to build a production dashboard. Copy the +`DASH` style block from `charts-histogram.py` as a starting point. + +## Histogram Property Reference + +| Property | Default | Notes | +|---|---|---| +| `chartType` | — | Must be `histogram` | +| `title` | — | Chart title text | +| `series1` | — | `"name:v1,v2,v3,..."` — raw values, not pre-binned | +| `binCount` | auto | Integer: force exactly N bins | +| `binSize` | auto | Number: force fixed bin width | +| `intervalClosed` | `r` | `r` = (a,b], `l` = [a,b) | +| `underflowBin` | — | Group values < N into a single ` M into a single `>M` bar | +| `gapWidth` | `0` | Space between bars (0 = touching) | +| `fill` | — | Single-color shortcut (HEX) | +| `colors` | — | Comma list of HEX (multi-series) | +| `dataLabels` | `false` | `true` puts value count above each bar | +| `datalabels.numfmt` | — | Excel format code (`0`, `0.0`, `0.00%`, `#,##0`) | +| `xAxisTitle` / `yAxisTitle` | — | Axis titles | +| `gridlines` | `true` | Value-axis major gridlines | +| `xGridlines` | `false` | Category-axis major gridlines | +| `tickLabels` | `true` | Show bin range labels on x-axis | +| `axismin` / `axismax` | — | Value-axis range (numeric) | +| `majorunit` / `minorunit` | — | Value-axis gridline interval | +| `axis.visible` / `cataxis.visible` / `valaxis.visible` | — | Axis hidden flags | +| `axisline` | — | Axis spine: `"color"` / `"color:width"` / `"color:width:dash"` / `"none"` | +| `cataxis.line` / `valaxis.line` | — | Per-axis spine styling | +| `plotareafill` / `plotfill` | — | Plot-area solid background color | +| `plotarea.border` / `plotborder` | — | Plot-area outline | +| `chartareafill` / `chartfill` | — | Chart-area solid background color | +| `chartarea.border` / `chartborder` | — | Chart-area outline | +| `series.shadow` | — | Outer shadow on bars: `"COLOR-BLUR-ANGLE-DIST-OPACITY"` | +| `title.shadow` | — | Outer shadow on title: `"COLOR-BLUR-ANGLE-DIST-OPACITY"` | +| `legend` | — | `top` / `bottom` / `left` / `right` / `none` | +| `legend.overlay` | `false` | Legend floats on top of plot area when `true` | +| `legendfont` | — | Compound `"size:color:fontname"` | +| `title.color` / `title.size` / `title.bold` / `title.font` | — | Chart title styling | +| `axisTitle.color` / `axisTitle.size` / `axisTitle.font` / `axisTitle.bold` | — | Axis title styling (both X and Y) | +| `axisfont` | — | Compound tick-label styling: `"size:color:fontname"` | +| `gridlineColor` | — | Value-axis major gridline color | +| `xGridlineColor` | — | Category-axis major gridline color (requires `xGridlines=true`) | +| `x` / `y` / `width` / `height` | — | Chart cell placement and size | + +## Inspect the Generated File + +```bash +# Count all charts across all sheets +officecli query charts-histogram.xlsx chart + +# Introspect a single chart's bound properties +officecli get charts-histogram.xlsx "/0-Hero/chart[1]" +officecli get charts-histogram.xlsx "/5-ML Dashboard/chart[1]" + +# Render any sheet to HTML preview +officecli view charts-histogram.xlsx html > preview.html +``` + +> Note: officecli's HTML preview renders the full parity vocabulary +> (plot-area / chart-area fills, gridline + axis line colors, tick +> label colors, data labels, locked axis scales, gapWidth, etc.), +> but does not currently reproduce custom axis-label font families — +> all tick labels fall back to the preview's default sans font. Excel +> renders the full styling including the font family. Use the preview +> for layout + color verification, use Excel (or Numbers / LibreOffice) +> for final typographic QA. diff --git a/examples/excel/charts/charts-histogram.py b/examples/excel/charts/charts-histogram.py new file mode 100644 index 000000000..dbdd9c9d6 --- /dev/null +++ b/examples/excel/charts/charts-histogram.py @@ -0,0 +1,1152 @@ +#!/usr/bin/env python3 +""" +Histogram Charts — Grand Showcase +================================== + +The most thorough, most visually polished histogram demo officecli can +produce. Every binning knob, every styling vocabulary, every canonical +distribution shape, six design themes on one dataset, four font type +specimens, and a cohesive production-grade ML dashboard — all driven by +real copyable officecli CLI commands. + +Generates: charts-histogram.xlsx (6 sheets, 29 histograms) + + 0-Hero 1 magazine-grade full-bleed hero poster chart + 1-Binning Lab 6 charts — every binning knob, identical styling + 2-Distribution Zoo 6 canonical real-world distribution shapes + 3-Theme Gallery 6 design themes on the SAME dataset + 4-Typography 4 font-family type specimens + 5-ML Dashboard 6-chart "Production ML Model Report" dashboard + +Usage: + python3 charts-histogram.py +""" + +import subprocess, os, atexit, random, math + +FILE = "charts-histogram.xlsx" + + +def cli(cmd): + """Run: officecli — prints stdout/stderr in real time.""" + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + + +# -------------------------------------------------------------------------- +# Scaffolding: create file, open it in resident mode (fast subsequent calls), +# and register a graceful close() on exit. +# -------------------------------------------------------------------------- +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + + +# -------------------------------------------------------------------------- +# Deterministic sample generators — same seed, same file every regeneration. +# All datasets are CSV-joined once here and reused across sheets. +# -------------------------------------------------------------------------- +def csv(values): + return ",".join(str(v) for v in values) + +# The "reference" bell curve — 200 samples around 75±12. Used by the hero, +# the binning lab, the theme gallery, the typography specimens, and the zoo. +random.seed(42) +BELL_200 = sorted(round(random.gauss(75, 12), 1) for _ in range(200)) +BELL_CSV = csv(BELL_200) + +# Bimodal: two cohorts (beginners ~55, experts ~88) glued together. +random.seed(7) +BIMODAL = sorted( + [round(random.gauss(55, 6), 1) for _ in range(80)] + + [round(random.gauss(88, 5), 1) for _ in range(80)] +) +BIMODAL_CSV = csv(BIMODAL) + +# Right-skewed / log-normal: classic income shape. +random.seed(11) +LOGNORM = sorted(round(math.exp(random.gauss(3.2, 0.55)), 1) for _ in range(180)) +LOGNORM_CSV = csv(LOGNORM) + +# Left-skewed: retirement ages — most cluster high, a few retire early. +random.seed(23) +LEFT_SKEW = sorted(round(75 - math.exp(random.gauss(1.6, 0.6)), 1) for _ in range(140)) +LEFT_CSV = csv(LEFT_SKEW) + +# Uniform: random draws evenly distributed across a range. +random.seed(31) +UNIFORM = sorted(round(random.uniform(0, 100), 1) for _ in range(160)) +UNIFORM_CSV = csv(UNIFORM) + +# Heavy-tailed (Pareto): most small, tiny fraction catastrophic. +random.seed(47) +PARETO = sorted(round(random.paretovariate(1.6) * 20, 1) for _ in range(200)) +PARETO_CSV = csv(PARETO) + +# --- ML Dashboard datasets (sheet 5) --- +random.seed(101) +LATENCY_MS = sorted(round(random.paretovariate(1.8) * 15 + 10, 1) for _ in range(250)) +LATENCY_CSV = csv(LATENCY_MS) + +random.seed(102) +CONFIDENCE = sorted(round(random.betavariate(6, 2) * 100, 2) for _ in range(240)) +CONFIDENCE_CSV = csv(CONFIDENCE) + +random.seed(103) +ERROR_MAG = sorted(round(abs(random.gauss(0, 1.5)), 3) for _ in range(180)) +ERROR_MAG_CSV = csv(ERROR_MAG) + +random.seed(104) +TOKEN_LEN = sorted( + [max(1, round(random.gauss(180, 40))) for _ in range(100)] + + [max(1, round(random.gauss(520, 90))) for _ in range(80)] +) +TOKEN_CSV = csv(TOKEN_LEN) + +random.seed(105) +GPU_UTIL = sorted(round(min(99.0, max(30.0, random.gauss(82, 8))), 1) for _ in range(200)) +GPU_CSV = csv(GPU_UTIL) + +random.seed(106) +COST_REQ = sorted(round(math.exp(random.gauss(-3.2, 0.9)) * 1000, 3) for _ in range(220)) +COST_CSV = csv(COST_REQ) + + +# ========================================================================== +# Sheet 0: "0-Hero" — the full-bleed magazine hero poster +# +# A single giant chart using EVERY histogram knob at once: +# - Dark "Midnight Academia" palette: navy plot area, gold bars, cream text +# - title.* (color/size/bold/font/shadow) +# - series.shadow + fill +# - axisline + axisfont + axisTitle.* +# - plotareafill / plotarea.border / chartareafill / chartarea.border +# - axismin / axismax / majorunit (locked Y scale) +# - gridlineColor +# - dataLabels + datalabels.numfmt +# - legend=top + legend.overlay + legendfont +# - intervalClosed=l + explicit binCount +# +# This chart is the "representative sample" — if it renders correctly, the +# entire histogram pipeline is healthy. +# ========================================================================== +print("\n--- 0-Hero ---") +cli(f'set "{FILE}" /Sheet1 --prop name="0-Hero"') + +# officecli add charts-histogram.xlsx "/0-Hero" --type chart \ +# --prop chartType=histogram \ +# --prop title="The Shape of Data · 200-sample bell curve" \ +# --prop title.color=F5F1E0 --prop title.size=22 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop "title.shadow=000000-8-45-4-70" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=24 --prop intervalClosed=l \ +# --prop fill=F0C96A --prop "series.shadow=000000-8-45-4-60" \ +# --prop axismin=0 --prop axismax=28 --prop majorunit=4 \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Frequency" \ +# --prop axisTitle.color=C9B87A --prop axisTitle.size=13 \ +# --prop axisTitle.bold=true --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=10:B8B090:Helvetica Neue" \ +# --prop "axisline=6A6448:1.5" \ +# --prop gridlineColor=2F3544 \ +# --prop plotareafill=1A1F2C --prop "plotarea.border=3A3E4E:1.25" \ +# --prop chartareafill=0B0F18 --prop "chartarea.border=2A2E3E:1" \ +# --prop dataLabels=true --prop "datalabels.numfmt=0" \ +# --prop legend=top --prop legend.overlay=false \ +# --prop "legendfont=11:D4C994:Helvetica Neue" \ +# --prop x=0 --prop y=0 --prop width=27 --prop height=38 +# Features: EVERY knob — title/series/axis/plotarea/chartarea/shadow/scaling/legend/datalabel +cli(f'add "{FILE}" "/0-Hero" --type chart' + f' --prop chartType=histogram' + f' --prop title="The Shape of Data · 200-sample bell curve"' + f' --prop title.color=F5F1E0 --prop title.size=22 --prop title.bold=true' + f' --prop title.font="Helvetica Neue"' + f' --prop "title.shadow=000000-8-45-4-70"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=24 --prop intervalClosed=l' + f' --prop fill=F0C96A --prop "series.shadow=000000-8-45-4-60"' + f' --prop axismin=0 --prop axismax=28 --prop majorunit=4' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Frequency"' + f' --prop axisTitle.color=C9B87A --prop axisTitle.size=13' + f' --prop axisTitle.bold=true --prop axisTitle.font="Helvetica Neue"' + f' --prop "axisfont=10:B8B090:Helvetica Neue"' + f' --prop "axisline=6A6448:1.5"' + f' --prop gridlineColor=2F3544' + f' --prop plotareafill=1A1F2C --prop "plotarea.border=3A3E4E:1.25"' + f' --prop chartareafill=0B0F18 --prop "chartarea.border=2A2E3E:1"' + f' --prop dataLabels=true --prop "datalabels.numfmt=0"' + f' --prop legend=top --prop legend.overlay=false' + f' --prop "legendfont=11:D4C994:Helvetica Neue"' + f' --prop x=0 --prop y=0 --prop width=27 --prop height=38') + + +# ========================================================================== +# Sheet 1: "1-Binning Lab" +# +# Six histograms, SAME dataset (BELL_200), IDENTICAL typography / colors / +# frames — the ONLY thing that varies is the binning strategy. Put side by +# side, this sheet is the "Rosetta stone": once you see how each binning +# knob reshapes the bars, you'll never be confused about which to use. +# +# ┌──────────┬──────────┐ +# │ 1. auto │ 2. count │ +# ├──────────┼──────────┤ +# │ 3. fine │ 4. width │ +# ├──────────┼──────────┤ +# │ 5. fence │ 6. lclos │ +# └──────────┴──────────┘ +# ========================================================================== +print("\n--- 1-Binning Lab ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Binning Lab"') + +# Shared "clean lab" style — every chart on this sheet wears the exact same +# outfit so the bin-shape difference is the only visible variable. +LAB = ( + ' --prop fill=4472C4' + ' --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true' + ' --prop title.font="Helvetica Neue"' + ' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + ' --prop axisTitle.color=6B7280 --prop axisTitle.size=10' + ' --prop axisTitle.font="Helvetica Neue"' + ' --prop "axisfont=9:6B7280:Helvetica Neue"' + ' --prop gridlineColor=F0F0F0' + ' --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75"' + ' --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75"' + ' --prop "axisline=9CA3AF:0.75"' +) + +# officecli add charts-histogram.xlsx "/1-Binning Lab" --type chart \ +# --prop chartType=histogram \ +# --prop title="1 · Auto-binning (Excel default)" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop fill=4472C4 \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=0 --prop y=0 --prop width=13 --prop height=18 +# Features: no binCount, no binSize — Excel picks the bin count automatically. +cli(f'add "{FILE}" "/1-Binning Lab" --type chart' + f' --prop chartType=histogram' + f' --prop title="1 · Auto-binning (Excel default)"' + f' --prop series1=Samples:{BELL_CSV}' + f'{LAB}' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/1-Binning Lab" --type chart \ +# --prop chartType=histogram \ +# --prop title="2 · binCount=8 (coarse)" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=8 \ +# --prop fill=4472C4 \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=14 --prop y=0 --prop width=13 --prop height=18 +# Features: binCount=8 — coarse. Fewer, wider bars. Good for "what's the mode?" +cli(f'add "{FILE}" "/1-Binning Lab" --type chart' + f' --prop chartType=histogram' + f' --prop title="2 · binCount=8 (coarse)"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=8' + f'{LAB}' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/1-Binning Lab" --type chart \ +# --prop chartType=histogram \ +# --prop title="3 · binCount=32 (fine)" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=32 \ +# --prop fill=4472C4 \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=0 --prop y=19 --prop width=13 --prop height=18 +# Features: binCount=32 — fine. Many narrow bars. Good for "is it really Gaussian?" +cli(f'add "{FILE}" "/1-Binning Lab" --type chart' + f' --prop chartType=histogram' + f' --prop title="3 · binCount=32 (fine)"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=32' + f'{LAB}' + f' --prop x=0 --prop y=19 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/1-Binning Lab" --type chart \ +# --prop chartType=histogram \ +# --prop title="4 · binSize=5 (fixed-width bins)" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binSize=5 \ +# --prop fill=4472C4 \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=14 --prop y=19 --prop width=13 --prop height=18 +# Features: binSize=5 — fixed bin width. Use when you want human-friendly +# bin boundaries (multiples of 5, 10, etc) regardless of data range. +cli(f'add "{FILE}" "/1-Binning Lab" --type chart' + f' --prop chartType=histogram' + f' --prop title="4 · binSize=5 (fixed-width bins)"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binSize=5' + f'{LAB}' + f' --prop x=14 --prop y=19 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/1-Binning Lab" --type chart \ +# --prop chartType=histogram \ +# --prop title="5 · underflow=55 · overflow=95 (fencing)" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binSize=5 --prop underflowBin=55 --prop overflowBin=95 \ +# --prop fill=4472C4 \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=0 --prop y=38 --prop width=13 --prop height=18 +# Features: underflowBin=55 + overflowBin=95 — outlier fencing. Everything +# below 55 or above 95 collapses into a single <55 / >95 bar. +cli(f'add "{FILE}" "/1-Binning Lab" --type chart' + f' --prop chartType=histogram' + f' --prop title="5 · underflow=55 · overflow=95 (fencing)"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binSize=5 --prop underflowBin=55 --prop overflowBin=95' + f'{LAB}' + f' --prop x=0 --prop y=38 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/1-Binning Lab" --type chart \ +# --prop chartType=histogram \ +# --prop title="6 · [a,b) intervals + gapWidth=30" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=16 --prop intervalClosed=l --prop gapWidth=30 \ +# --prop fill=4472C4 \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=14 --prop y=38 --prop width=13 --prop height=18 +# Features: intervalClosed=l (half-open [a,b)) + gapWidth=30 — shows the +# "left-closed" variant AND pushes bars apart so you can see each one. +# Useful when the dataset has values lying exactly on a bin boundary. +cli(f'add "{FILE}" "/1-Binning Lab" --type chart' + f' --prop chartType=histogram' + f' --prop title="6 · [a,b) intervals + gapWidth=30"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=16 --prop intervalClosed=l --prop gapWidth=30' + f'{LAB}' + f' --prop x=14 --prop y=38 --prop width=13 --prop height=18') + + +# ========================================================================== +# Sheet 2: "2-Distribution Zoo" +# +# A cohesive 2x3 gallery of the canonical distribution shapes you'll see +# in production data. Pattern recognition: if you ever see one of these +# shapes in a telemetry chart, you know immediately what's going on. +# +# Every chart shares the same typography + plot/chart area frames; only +# the fill color and data change. Uses different binning strategies +# appropriate to each distribution. +# ========================================================================== +print("\n--- 2-Distribution Zoo ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Distribution Zoo"') + +ZOO = ( + ' --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true' + ' --prop title.font="Helvetica Neue"' + ' --prop axisTitle.color=6B7280 --prop axisTitle.size=10' + ' --prop axisTitle.font="Helvetica Neue"' + ' --prop "axisfont=9:6B7280:Helvetica Neue"' + ' --prop gridlineColor=EFEFEF' + ' --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75"' + ' --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75"' + ' --prop "axisline=9CA3AF:0.75"' +) + +# officecli add charts-histogram.xlsx "/2-Distribution Zoo" --type chart \ +# --prop chartType=histogram \ +# --prop title="Normal · bell curve (reference)" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=2F5597 \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=EFEFEF \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=0 --prop y=0 --prop width=13 --prop height=18 +# Features: classic bell curve reference, binCount=18, midnight blue fill. +cli(f'add "{FILE}" "/2-Distribution Zoo" --type chart' + f' --prop chartType=histogram' + f' --prop title="Normal · bell curve (reference)"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=2F5597' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f'{ZOO}' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/2-Distribution Zoo" --type chart \ +# --prop chartType=histogram \ +# --prop title="Bimodal · two hidden cohorts" \ +# --prop series1="Score:<160 bimodal values>" \ +# --prop binCount=22 --prop fill=ED7D31 \ +# --prop xAxisTitle="Test score" --prop yAxisTitle="Students" \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=EFEFEF \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=14 --prop y=0 --prop width=13 --prop height=18 +# Features: bimodal — two hidden populations. Narrow bins reveal the split. +cli(f'add "{FILE}" "/2-Distribution Zoo" --type chart' + f' --prop chartType=histogram' + f' --prop title="Bimodal · two hidden cohorts"' + f' --prop series1=Score:{BIMODAL_CSV}' + f' --prop binCount=22 --prop fill=ED7D31' + f' --prop xAxisTitle="Test score" --prop yAxisTitle="Students"' + f'{ZOO}' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/2-Distribution Zoo" --type chart \ +# --prop chartType=histogram \ +# --prop title="Right-skewed · log-normal (income)" \ +# --prop series1="Income:<180 log-normal values>" \ +# --prop binCount=20 --prop fill=70AD47 \ +# --prop xAxisTitle="Monthly income ($k)" --prop yAxisTitle="People" \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=EFEFEF \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=0 --prop y=19 --prop width=13 --prop height=18 +# Features: right-skewed log-normal. Mean >> median, long tail to the right. +cli(f'add "{FILE}" "/2-Distribution Zoo" --type chart' + f' --prop chartType=histogram' + f' --prop title="Right-skewed · log-normal (income)"' + f' --prop series1=Income:{LOGNORM_CSV}' + f' --prop binCount=20 --prop fill=70AD47' + f' --prop xAxisTitle="Monthly income ($k)" --prop yAxisTitle="People"' + f'{ZOO}' + f' --prop x=0 --prop y=19 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/2-Distribution Zoo" --type chart \ +# --prop chartType=histogram \ +# --prop title="Left-skewed · retirement ages" \ +# --prop series1="Age:<140 left-skewed values>" \ +# --prop binCount=18 --prop fill=7030A0 \ +# --prop xAxisTitle="Age at retirement" --prop yAxisTitle="Retirees" \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=EFEFEF \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=14 --prop y=19 --prop width=13 --prop height=18 +# Features: left-skewed — retirement ages cluster high, tail stretches left. +cli(f'add "{FILE}" "/2-Distribution Zoo" --type chart' + f' --prop chartType=histogram' + f' --prop title="Left-skewed · retirement ages"' + f' --prop series1=Age:{LEFT_CSV}' + f' --prop binCount=18 --prop fill=7030A0' + f' --prop xAxisTitle="Age at retirement" --prop yAxisTitle="Retirees"' + f'{ZOO}' + f' --prop x=14 --prop y=19 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/2-Distribution Zoo" --type chart \ +# --prop chartType=histogram \ +# --prop title="Uniform · flat floor" \ +# --prop series1="Draws:<160 uniform values>" \ +# --prop binSize=10 --prop fill=00B0F0 \ +# --prop xAxisTitle="Random draw (0-100)" --prop yAxisTitle="Count" \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=EFEFEF \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=0 --prop y=38 --prop width=13 --prop height=18 +# Features: uniform — every value equally likely. binSize emphasizes the +# "flat floor" visual tell. +cli(f'add "{FILE}" "/2-Distribution Zoo" --type chart' + f' --prop chartType=histogram' + f' --prop title="Uniform · flat floor"' + f' --prop series1=Draws:{UNIFORM_CSV}' + f' --prop binSize=10 --prop fill=00B0F0' + f' --prop xAxisTitle="Random draw (0-100)" --prop yAxisTitle="Count"' + f'{ZOO}' + f' --prop x=0 --prop y=38 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/2-Distribution Zoo" --type chart \ +# --prop chartType=histogram \ +# --prop title="Heavy-tailed · Pareto (overflow=250)" \ +# --prop series1="Latency:<200 Pareto values>" \ +# --prop binSize=20 --prop overflowBin=250 --prop fill=C00000 \ +# --prop xAxisTitle="Latency (ms)" --prop yAxisTitle="Requests" \ +# --prop title.color=1F2937 --prop title.size=13 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=9:6B7280:Helvetica Neue" \ +# --prop gridlineColor=EFEFEF \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=14 --prop y=38 --prop width=13 --prop height=18 +# Features: heavy-tailed Pareto + overflowBin. Fences the catastrophic tail +# so the interesting bulk of the distribution stays readable. +cli(f'add "{FILE}" "/2-Distribution Zoo" --type chart' + f' --prop chartType=histogram' + f' --prop title="Heavy-tailed · Pareto (overflow=250)"' + f' --prop series1=Latency:{PARETO_CSV}' + f' --prop binSize=20 --prop overflowBin=250 --prop fill=C00000' + f' --prop xAxisTitle="Latency (ms)" --prop yAxisTitle="Requests"' + f'{ZOO}' + f' --prop x=14 --prop y=38 --prop width=13 --prop height=18') + + +# ========================================================================== +# Sheet 3: "3-Theme Gallery" +# +# Six complete design themes applied to the SAME bell-curve dataset. Each +# theme is a coordinated palette: plot-area fill, chart-area fill, series +# fill, gridline color, axis line color, tick-label color, title color, +# title font — all chosen to read as one coherent mood. +# +# Grid: +# ┌─────────────┬─────────────┐ +# │ 1. Midnight │ 2. Sunset │ +# ├─────────────┼─────────────┤ +# │ 3. Forest │ 4. Mono │ +# ├─────────────┼─────────────┤ +# │ 5. Neon │ 6. Pastel │ +# └─────────────┴─────────────┘ +# ========================================================================== +print("\n--- 3-Theme Gallery ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Theme Gallery"') + +# officecli add charts-histogram.xlsx "/3-Theme Gallery" --type chart \ +# --prop chartType=histogram \ +# --prop title="Midnight Academia" \ +# --prop title.color=F5F1E0 --prop title.size=14 --prop title.bold=true \ +# --prop title.font="Georgia" \ +# --prop "title.shadow=000000-6-45-3-70" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=F0C96A \ +# --prop "series.shadow=000000-6-45-3-55" \ +# --prop plotareafill=1A1F2C --prop "plotarea.border=3A3E4E:1" \ +# --prop chartareafill=0B0F18 --prop "chartarea.border=2A2E3E:0.75" \ +# --prop gridlineColor=2F3544 \ +# --prop "axisfont=9:B8B090:Georgia" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=C9B87A --prop axisTitle.size=10 \ +# --prop axisTitle.font="Georgia" \ +# --prop "axisline=5A5848:1" \ +# --prop x=0 --prop y=0 --prop width=13 --prop height=18 +# Features: dark plot area, gold bars, series.shadow, title.shadow +cli(f'add "{FILE}" "/3-Theme Gallery" --type chart' + f' --prop chartType=histogram' + f' --prop title="Midnight Academia"' + f' --prop title.color=F5F1E0 --prop title.size=14 --prop title.bold=true' + f' --prop title.font="Georgia"' + f' --prop "title.shadow=000000-6-45-3-70"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=F0C96A' + f' --prop "series.shadow=000000-6-45-3-55"' + f' --prop plotareafill=1A1F2C --prop "plotarea.border=3A3E4E:1"' + f' --prop chartareafill=0B0F18 --prop "chartarea.border=2A2E3E:0.75"' + f' --prop gridlineColor=2F3544' + f' --prop "axisfont=9:B8B090:Georgia"' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f' --prop axisTitle.color=C9B87A --prop axisTitle.size=10' + f' --prop axisTitle.font="Georgia"' + f' --prop "axisline=5A5848:1"' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/3-Theme Gallery" --type chart \ +# --prop chartType=histogram \ +# --prop title="Sunset Terracotta" \ +# --prop title.color=3F2818 --prop title.size=14 --prop title.bold=true \ +# --prop title.font="Georgia" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=E85D4A \ +# --prop plotareafill=FFF5E8 --prop "plotarea.border=F0D8B0:1" \ +# --prop chartareafill=FFE6C7 --prop "chartarea.border=E6BC88:1" \ +# --prop gridlineColor=F5C98A \ +# --prop "axisfont=9:6B4A2A:Georgia" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=A8522C --prop axisTitle.size=10 \ +# --prop axisTitle.font="Georgia" \ +# --prop "axisline=C08050:1" \ +# --prop x=14 --prop y=0 --prop width=13 --prop height=18 +# Theme 2 · Sunset Terracotta (warm cream + coral, serif) +cli(f'add "{FILE}" "/3-Theme Gallery" --type chart' + f' --prop chartType=histogram' + f' --prop title="Sunset Terracotta"' + f' --prop title.color=3F2818 --prop title.size=14 --prop title.bold=true' + f' --prop title.font="Georgia"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=E85D4A' + f' --prop plotareafill=FFF5E8 --prop "plotarea.border=F0D8B0:1"' + f' --prop chartareafill=FFE6C7 --prop "chartarea.border=E6BC88:1"' + f' --prop gridlineColor=F5C98A' + f' --prop "axisfont=9:6B4A2A:Georgia"' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f' --prop axisTitle.color=A8522C --prop axisTitle.size=10' + f' --prop axisTitle.font="Georgia"' + f' --prop "axisline=C08050:1"' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/3-Theme Gallery" --type chart \ +# --prop chartType=histogram \ +# --prop title="Forest Parchment" \ +# --prop title.color=1F3A1F --prop title.size=14 --prop title.bold=true \ +# --prop title.font="Georgia" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=2F5D3A \ +# --prop plotareafill=F3EDD8 --prop "plotarea.border=C8B890:1" \ +# --prop chartareafill=EADFBE --prop "chartarea.border=A89858:1" \ +# --prop gridlineColor=C0B888 \ +# --prop "axisfont=9:4A5A3A:Georgia" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=3F5A2F --prop axisTitle.size=10 \ +# --prop axisTitle.font="Georgia" \ +# --prop "axisline=6A7A4A:1" \ +# --prop x=0 --prop y=19 --prop width=13 --prop height=18 +# Theme 3 · Forest Parchment (beige + forest green, serif) +cli(f'add "{FILE}" "/3-Theme Gallery" --type chart' + f' --prop chartType=histogram' + f' --prop title="Forest Parchment"' + f' --prop title.color=1F3A1F --prop title.size=14 --prop title.bold=true' + f' --prop title.font="Georgia"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=2F5D3A' + f' --prop plotareafill=F3EDD8 --prop "plotarea.border=C8B890:1"' + f' --prop chartareafill=EADFBE --prop "chartarea.border=A89858:1"' + f' --prop gridlineColor=C0B888' + f' --prop "axisfont=9:4A5A3A:Georgia"' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f' --prop axisTitle.color=3F5A2F --prop axisTitle.size=10' + f' --prop axisTitle.font="Georgia"' + f' --prop "axisline=6A7A4A:1"' + f' --prop x=0 --prop y=19 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/3-Theme Gallery" --type chart \ +# --prop chartType=histogram \ +# --prop title="Editorial Mono" \ +# --prop title.color=111111 --prop title.size=14 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=2A2A2A \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=CCCCCC:0.75" \ +# --prop chartareafill=FAFAFA --prop "chartarea.border=E0E0E0:0.75" \ +# --prop gridlineColor=EEEEEE \ +# --prop "axisfont=9:555555:Helvetica Neue" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=333333 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisline=888888:1" \ +# --prop x=14 --prop y=19 --prop width=13 --prop height=18 +# Theme 4 · Editorial Mono (pure grayscale, sans) +cli(f'add "{FILE}" "/3-Theme Gallery" --type chart' + f' --prop chartType=histogram' + f' --prop title="Editorial Mono"' + f' --prop title.color=111111 --prop title.size=14 --prop title.bold=true' + f' --prop title.font="Helvetica Neue"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=2A2A2A' + f' --prop plotareafill=FFFFFF --prop "plotarea.border=CCCCCC:0.75"' + f' --prop chartareafill=FAFAFA --prop "chartarea.border=E0E0E0:0.75"' + f' --prop gridlineColor=EEEEEE' + f' --prop "axisfont=9:555555:Helvetica Neue"' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f' --prop axisTitle.color=333333 --prop axisTitle.size=10' + f' --prop axisTitle.font="Helvetica Neue"' + f' --prop "axisline=888888:1"' + f' --prop x=14 --prop y=19 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/3-Theme Gallery" --type chart \ +# --prop chartType=histogram \ +# --prop title="Neon Terminal" \ +# --prop title.color=00F0C8 --prop title.size=14 --prop title.bold=true \ +# --prop title.font="Courier New" \ +# --prop "title.shadow=00F0C8-6-45-0-40" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=00F0C8 \ +# --prop "series.shadow=00F0C8-8-45-0-45" \ +# --prop plotareafill=0A0A14 --prop "plotarea.border=1F2F3F:1" \ +# --prop chartareafill=000008 --prop "chartarea.border=1F1F2F:1" \ +# --prop gridlineColor=1A2A3A \ +# --prop "axisfont=9:00D0E8:Courier New" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=00D0E8 --prop axisTitle.size=10 \ +# --prop axisTitle.font="Courier New" \ +# --prop "axisline=00707F:1" \ +# --prop x=0 --prop y=38 --prop width=13 --prop height=18 +# Theme 5 · Neon Terminal (black + electric cyan, mono) +cli(f'add "{FILE}" "/3-Theme Gallery" --type chart' + f' --prop chartType=histogram' + f' --prop title="Neon Terminal"' + f' --prop title.color=00F0C8 --prop title.size=14 --prop title.bold=true' + f' --prop title.font="Courier New"' + f' --prop "title.shadow=00F0C8-6-45-0-40"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=00F0C8' + f' --prop "series.shadow=00F0C8-8-45-0-45"' + f' --prop plotareafill=0A0A14 --prop "plotarea.border=1F2F3F:1"' + f' --prop chartareafill=000008 --prop "chartarea.border=1F1F2F:1"' + f' --prop gridlineColor=1A2A3A' + f' --prop "axisfont=9:00D0E8:Courier New"' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f' --prop axisTitle.color=00D0E8 --prop axisTitle.size=10' + f' --prop axisTitle.font="Courier New"' + f' --prop "axisline=00707F:1"' + f' --prop x=0 --prop y=38 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/3-Theme Gallery" --type chart \ +# --prop chartType=histogram \ +# --prop title="Pastel Bloom" \ +# --prop title.color=5A3C4A --prop title.size=14 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=F5A7C8 \ +# --prop plotareafill=FDF4F8 --prop "plotarea.border=F0D0E0:1" \ +# --prop chartareafill=FAEDF2 --prop "chartarea.border=F0C0D8:1" \ +# --prop gridlineColor=F5D8E5 \ +# --prop "axisfont=9:8A6878:Helvetica Neue" \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=A04C6A --prop axisTitle.size=10 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisline=C888A0:1" \ +# --prop x=14 --prop y=38 --prop width=13 --prop height=18 +# Theme 6 · Pastel Bloom (lavender cream + rose, sans) +cli(f'add "{FILE}" "/3-Theme Gallery" --type chart' + f' --prop chartType=histogram' + f' --prop title="Pastel Bloom"' + f' --prop title.color=5A3C4A --prop title.size=14 --prop title.bold=true' + f' --prop title.font="Helvetica Neue"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=F5A7C8' + f' --prop plotareafill=FDF4F8 --prop "plotarea.border=F0D0E0:1"' + f' --prop chartareafill=FAEDF2 --prop "chartarea.border=F0C0D8:1"' + f' --prop gridlineColor=F5D8E5' + f' --prop "axisfont=9:8A6878:Helvetica Neue"' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f' --prop axisTitle.color=A04C6A --prop axisTitle.size=10' + f' --prop axisTitle.font="Helvetica Neue"' + f' --prop "axisline=C888A0:1"' + f' --prop x=14 --prop y=38 --prop width=13 --prop height=18') + + +# ========================================================================== +# Sheet 4: "4-Typography" +# +# Four font-family "type specimens". Same data, same geometry, same colors — +# only the font varies. Side-by-side, this shows how typography alone reads +# as tone: Helvetica is corporate, Georgia is editorial, Courier is data, +# Verdana is approachable. +# ========================================================================== +print("\n--- 4-Typography ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Typography"') + +# officecli add charts-histogram.xlsx "/4-Typography" --type chart \ +# --prop chartType=histogram \ +# --prop title="Helvetica Neue · modern sans" \ +# --prop title.color=1F2937 --prop title.size=16 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=4472C4 \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=4472C4 --prop axisTitle.size=11 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=10:6B7280:Helvetica Neue" \ +# --prop gridlineColor=EEEEEE \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop x=0 --prop y=0 --prop width=13 --prop height=18 +# Specimen 1 · Helvetica Neue (modern sans — dashboards, corporate reports) +cli(f'add "{FILE}" "/4-Typography" --type chart' + f' --prop chartType=histogram' + f' --prop title="Helvetica Neue · modern sans"' + f' --prop title.color=1F2937 --prop title.size=16 --prop title.bold=true' + f' --prop title.font="Helvetica Neue"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=4472C4' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f' --prop axisTitle.color=4472C4 --prop axisTitle.size=11' + f' --prop axisTitle.font="Helvetica Neue"' + f' --prop "axisfont=10:6B7280:Helvetica Neue"' + f' --prop gridlineColor=EEEEEE' + f' --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75"' + f' --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75"' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/4-Typography" --type chart \ +# --prop chartType=histogram \ +# --prop title="Georgia · editorial serif" \ +# --prop title.color=3F2818 --prop title.size=16 --prop title.bold=true \ +# --prop title.font="Georgia" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=A8522C \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=A8522C --prop axisTitle.size=11 \ +# --prop axisTitle.font="Georgia" \ +# --prop "axisfont=10:6B4A2A:Georgia" \ +# --prop gridlineColor=F0E8D8 \ +# --prop plotareafill=FFFBF3 --prop "plotarea.border=E8D8B8:0.75" \ +# --prop chartareafill=FDF6E8 --prop "chartarea.border=E8D8B8:0.75" \ +# --prop x=14 --prop y=0 --prop width=13 --prop height=18 +# Specimen 2 · Georgia (editorial serif — magazines, long-form reports) +cli(f'add "{FILE}" "/4-Typography" --type chart' + f' --prop chartType=histogram' + f' --prop title="Georgia · editorial serif"' + f' --prop title.color=3F2818 --prop title.size=16 --prop title.bold=true' + f' --prop title.font="Georgia"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=A8522C' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f' --prop axisTitle.color=A8522C --prop axisTitle.size=11' + f' --prop axisTitle.font="Georgia"' + f' --prop "axisfont=10:6B4A2A:Georgia"' + f' --prop gridlineColor=F0E8D8' + f' --prop plotareafill=FFFBF3 --prop "plotarea.border=E8D8B8:0.75"' + f' --prop chartareafill=FDF6E8 --prop "chartarea.border=E8D8B8:0.75"' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/4-Typography" --type chart \ +# --prop chartType=histogram \ +# --prop title="Courier New · data mono" \ +# --prop title.color=1A3A1A --prop title.size=16 --prop title.bold=true \ +# --prop title.font="Courier New" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=2F8F4F \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=2F8F4F --prop axisTitle.size=11 \ +# --prop axisTitle.font="Courier New" \ +# --prop "axisfont=10:3A5A3A:Courier New" \ +# --prop gridlineColor=E0EDE0 \ +# --prop plotareafill=F7FBF7 --prop "plotarea.border=C8DCC8:0.75" \ +# --prop chartareafill=F0F7F0 --prop "chartarea.border=C8DCC8:0.75" \ +# --prop x=0 --prop y=19 --prop width=13 --prop height=18 +# Specimen 3 · Courier New (monospace — data, telemetry, engineering) +cli(f'add "{FILE}" "/4-Typography" --type chart' + f' --prop chartType=histogram' + f' --prop title="Courier New · data mono"' + f' --prop title.color=1A3A1A --prop title.size=16 --prop title.bold=true' + f' --prop title.font="Courier New"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=2F8F4F' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f' --prop axisTitle.color=2F8F4F --prop axisTitle.size=11' + f' --prop axisTitle.font="Courier New"' + f' --prop "axisfont=10:3A5A3A:Courier New"' + f' --prop gridlineColor=E0EDE0' + f' --prop plotareafill=F7FBF7 --prop "plotarea.border=C8DCC8:0.75"' + f' --prop chartareafill=F0F7F0 --prop "chartarea.border=C8DCC8:0.75"' + f' --prop x=0 --prop y=19 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/4-Typography" --type chart \ +# --prop chartType=histogram \ +# --prop title="Verdana · friendly sans" \ +# --prop title.color=4A2B6A --prop title.size=16 --prop title.bold=true \ +# --prop title.font="Verdana" \ +# --prop series1="Samples:<200 bell values>" \ +# --prop binCount=18 --prop fill=8E4DBB \ +# --prop xAxisTitle="Score" --prop yAxisTitle="Count" \ +# --prop axisTitle.color=8E4DBB --prop axisTitle.size=11 \ +# --prop axisTitle.font="Verdana" \ +# --prop "axisfont=10:6B4A8A:Verdana" \ +# --prop gridlineColor=ECE0F4 \ +# --prop plotareafill=FCF7FF --prop "plotarea.border=D8C4E8:0.75" \ +# --prop chartareafill=F6EDFA --prop "chartarea.border=D8C4E8:0.75" \ +# --prop x=14 --prop y=19 --prop width=13 --prop height=18 +# Specimen 4 · Verdana (friendly sans — onboarding, public-facing UI) +cli(f'add "{FILE}" "/4-Typography" --type chart' + f' --prop chartType=histogram' + f' --prop title="Verdana · friendly sans"' + f' --prop title.color=4A2B6A --prop title.size=16 --prop title.bold=true' + f' --prop title.font="Verdana"' + f' --prop series1=Samples:{BELL_CSV}' + f' --prop binCount=18 --prop fill=8E4DBB' + f' --prop xAxisTitle="Score" --prop yAxisTitle="Count"' + f' --prop axisTitle.color=8E4DBB --prop axisTitle.size=11' + f' --prop axisTitle.font="Verdana"' + f' --prop "axisfont=10:6B4A8A:Verdana"' + f' --prop gridlineColor=ECE0F4' + f' --prop plotareafill=FCF7FF --prop "plotarea.border=D8C4E8:0.75"' + f' --prop chartareafill=F6EDFA --prop "chartarea.border=D8C4E8:0.75"' + f' --prop x=14 --prop y=19 --prop width=13 --prop height=18') + + +# ========================================================================== +# Sheet 5: "5-ML Dashboard" +# +# A cohesive six-chart "Production ML Model Report". Every chart wears the +# same corporate dashboard uniform — same typography, same frames, same +# gridlines — but each shows a different slice of the model's behavior, +# deliberately using a different color + binning strategy so the six read +# as a single dashboard at a glance. +# +# Row 1: Inference latency (ms) | Prediction confidence (%) +# Row 2: |Residual| (logit) | Token length (chars) +# Row 3: GPU utilization (%) | Cost per request ($ × 0.001) +# ========================================================================== +print("\n--- 5-ML Dashboard ---") +cli(f'add "{FILE}" / --type sheet --prop name="5-ML Dashboard"') + +DASH = ( + ' --prop title.color=1F2937 --prop title.size=12 --prop title.bold=true' + ' --prop title.font="Helvetica Neue"' + ' --prop axisTitle.color=6B7280 --prop axisTitle.size=9' + ' --prop axisTitle.font="Helvetica Neue"' + ' --prop "axisfont=8:6B7280:Helvetica Neue"' + ' --prop gridlineColor=F0F0F0' + ' --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75"' + ' --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75"' + ' --prop "axisline=9CA3AF:0.75"' + ' --prop dataLabels=false' +) + +# officecli add charts-histogram.xlsx "/5-ML Dashboard" --type chart \ +# --prop chartType=histogram \ +# --prop title="Inference Latency · p50-p99 (ms)" \ +# --prop series1="Latency:<250 Pareto latency values>" \ +# --prop binSize=25 --prop overflowBin=300 --prop fill=EF4444 \ +# --prop "series.shadow=EF4444-4-45-2-25" \ +# --prop xAxisTitle="Latency (ms)" --prop yAxisTitle="Requests" \ +# --prop title.color=1F2937 --prop title.size=12 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=9 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=8:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop dataLabels=false \ +# --prop x=0 --prop y=0 --prop width=13 --prop height=18 +# 1 · Inference Latency — heavy-tail, overflow-fenced, red for "watch this" +cli(f'add "{FILE}" "/5-ML Dashboard" --type chart' + f' --prop chartType=histogram' + f' --prop title="Inference Latency · p50-p99 (ms)"' + f' --prop series1=Latency:{LATENCY_CSV}' + f' --prop binSize=25 --prop overflowBin=300 --prop fill=EF4444' + f' --prop "series.shadow=EF4444-4-45-2-25"' + f' --prop xAxisTitle="Latency (ms)" --prop yAxisTitle="Requests"' + f'{DASH}' + f' --prop x=0 --prop y=0 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/5-ML Dashboard" --type chart \ +# --prop chartType=histogram \ +# --prop title="Prediction Confidence" \ +# --prop series1="Confidence:<240 beta confidence values>" \ +# --prop binSize=5 --prop fill=10B981 \ +# --prop axismin=0 --prop majorunit=50 \ +# --prop xAxisTitle="Softmax confidence (%)" --prop yAxisTitle="Samples" \ +# --prop title.color=1F2937 --prop title.size=12 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=9 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=8:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop dataLabels=false \ +# --prop x=14 --prop y=0 --prop width=13 --prop height=18 +# 2 · Prediction Confidence — beta-like, axismin/max locked to 0..100 +cli(f'add "{FILE}" "/5-ML Dashboard" --type chart' + f' --prop chartType=histogram' + f' --prop title="Prediction Confidence"' + f' --prop series1=Confidence:{CONFIDENCE_CSV}' + f' --prop binSize=5 --prop fill=10B981' + f' --prop axismin=0 --prop majorunit=50' + f' --prop xAxisTitle="Softmax confidence (%)" --prop yAxisTitle="Samples"' + f'{DASH}' + f' --prop x=14 --prop y=0 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/5-ML Dashboard" --type chart \ +# --prop chartType=histogram \ +# --prop title="|Residual| · model calibration" \ +# --prop series1="Residual:<180 half-normal error values>" \ +# --prop binSize=0.25 --prop intervalClosed=l --prop fill=F59E0B \ +# --prop xAxisTitle="|y - ŷ| (logit)" --prop yAxisTitle="Samples" \ +# --prop title.color=1F2937 --prop title.size=12 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=9 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=8:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop dataLabels=false \ +# --prop x=0 --prop y=19 --prop width=13 --prop height=18 +# 3 · Residual Magnitude — half-normal, intervalClosed=l so bin=0 catches zeros +cli(f'add "{FILE}" "/5-ML Dashboard" --type chart' + f' --prop chartType=histogram' + f' --prop title="|Residual| · model calibration"' + f' --prop series1=Residual:{ERROR_MAG_CSV}' + f' --prop binSize=0.25 --prop intervalClosed=l --prop fill=F59E0B' + f' --prop xAxisTitle="|y - ŷ| (logit)" --prop yAxisTitle="Samples"' + f'{DASH}' + f' --prop x=0 --prop y=19 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/5-ML Dashboard" --type chart \ +# --prop chartType=histogram \ +# --prop title="Token Length · short vs long prompts" \ +# --prop series1="Tokens:<180 bimodal token-length values>" \ +# --prop binCount=24 --prop fill=6366F1 \ +# --prop xAxisTitle="Tokens" --prop yAxisTitle="Requests" \ +# --prop title.color=1F2937 --prop title.size=12 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=9 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=8:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop dataLabels=false \ +# --prop x=14 --prop y=19 --prop width=13 --prop height=18 +# 4 · Token Length — bimodal (short prompts vs long prompts) +cli(f'add "{FILE}" "/5-ML Dashboard" --type chart' + f' --prop chartType=histogram' + f' --prop title="Token Length · short vs long prompts"' + f' --prop series1=Tokens:{TOKEN_CSV}' + f' --prop binCount=24 --prop fill=6366F1' + f' --prop xAxisTitle="Tokens" --prop yAxisTitle="Requests"' + f'{DASH}' + f' --prop x=14 --prop y=19 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/5-ML Dashboard" --type chart \ +# --prop chartType=histogram \ +# --prop title="GPU Utilization" \ +# --prop series1="GPU:<200 normal GPU utilization values>" \ +# --prop binSize=5 --prop fill=8B5CF6 \ +# --prop axismin=0 --prop axismax=50 --prop majorunit=10 \ +# --prop xAxisTitle="Utilization (%)" --prop yAxisTitle="Samples" \ +# --prop title.color=1F2937 --prop title.size=12 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=9 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=8:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop dataLabels=false \ +# --prop x=0 --prop y=38 --prop width=13 --prop height=18 +# 5 · GPU Utilization — locked axis range so dashboard charts share scale +cli(f'add "{FILE}" "/5-ML Dashboard" --type chart' + f' --prop chartType=histogram' + f' --prop title="GPU Utilization"' + f' --prop series1=GPU:{GPU_CSV}' + f' --prop binSize=5 --prop fill=8B5CF6' + f' --prop axismin=0 --prop axismax=50 --prop majorunit=10' + f' --prop xAxisTitle="Utilization (%)" --prop yAxisTitle="Samples"' + f'{DASH}' + f' --prop x=0 --prop y=38 --prop width=13 --prop height=18') + +# officecli add charts-histogram.xlsx "/5-ML Dashboard" --type chart \ +# --prop chartType=histogram \ +# --prop title="Cost per Request ($ × 0.001)" \ +# --prop series1="Cost:<220 log-normal cost values>" \ +# --prop binSize=5 --prop overflowBin=120 --prop fill=EC4899 \ +# --prop dataLabels=true --prop "datalabels.numfmt=0" \ +# --prop xAxisTitle="Cost (m$)" --prop yAxisTitle="Requests" \ +# --prop title.color=1F2937 --prop title.size=12 --prop title.bold=true \ +# --prop title.font="Helvetica Neue" \ +# --prop axisTitle.color=6B7280 --prop axisTitle.size=9 \ +# --prop axisTitle.font="Helvetica Neue" \ +# --prop "axisfont=8:6B7280:Helvetica Neue" \ +# --prop gridlineColor=F0F0F0 \ +# --prop plotareafill=FFFFFF --prop "plotarea.border=E5E7EB:0.75" \ +# --prop chartareafill=F9FAFB --prop "chartarea.border=E5E7EB:0.75" \ +# --prop "axisline=9CA3AF:0.75" \ +# --prop x=14 --prop y=38 --prop width=13 --prop height=18 +# 6 · Cost per Request — log-normal, overflow-fenced, data labels with numfmt +cli(f'add "{FILE}" "/5-ML Dashboard" --type chart' + f' --prop chartType=histogram' + f' --prop title="Cost per Request ($ × 0.001)"' + f' --prop series1=Cost:{COST_CSV}' + f' --prop binSize=5 --prop overflowBin=120 --prop fill=EC4899' + f' --prop dataLabels=true --prop "datalabels.numfmt=0"' + f' --prop xAxisTitle="Cost (m$)" --prop yAxisTitle="Requests"' + f'{DASH}' + f' --prop x=14 --prop y=38 --prop width=13 --prop height=18') + + +print(f"\nDone! Generated: {FILE}") +print(" 6 sheets, 29 histograms total") +print(" Sheet 0 (0-Hero): 1 magazine-grade full-bleed hero poster") +print(" Sheet 1 (1-Binning Lab): 6 charts — every binning knob, identical styling") +print(" Sheet 2 (2-Distribution Zoo): 6 canonical real-world distribution shapes") +print(" Sheet 3 (3-Theme Gallery): 6 design themes on the SAME dataset") +print(" Sheet 4 (4-Typography): 4 font-family type specimens") +print(" Sheet 5 (5-ML Dashboard): 6-chart Production ML Model Report") diff --git a/examples/excel/charts/charts-histogram.xlsx b/examples/excel/charts/charts-histogram.xlsx new file mode 100644 index 000000000..33e19b909 Binary files /dev/null and b/examples/excel/charts/charts-histogram.xlsx differ diff --git a/examples/excel/charts/charts-line.md b/examples/excel/charts/charts-line.md new file mode 100644 index 000000000..993cf16f9 --- /dev/null +++ b/examples/excel/charts/charts-line.md @@ -0,0 +1,292 @@ +# Line Charts Showcase + +This demo consists of three files that work together: + +- **charts-line.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-line.xlsx** — The generated workbook with 8 sheets (1 data + 7 chart sheets, 28 charts total). +- **charts-line.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-line.py +# → charts-line.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Line Fundamentals + +Four basic line charts covering every data input method and marker fundamentals. + +```bash +# Inline named series with axis titles +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop series1="Product A:120,180,210,250" \ + --prop series2="Product B:90,140,160,200" \ + --prop categories=Q1,Q2,Q3,Q4 \ + --prop colors=4472C4,ED7D31,70AD47 \ + --prop catTitle=Quarter --prop axisTitle=Revenue \ + --prop axisfont=9:58626E:Arial --prop gridlines=D9D9D9:0.5:dot + +# Cell-range series (dotted syntax) with markers +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop series1.name=East \ + --prop series1.values=Sheet1!B2:B13 \ + --prop series1.categories=Sheet1!A2:A13 \ + --prop showMarkers=true --prop marker=circle:6:2E75B6 \ + --prop minorGridlines=EEEEEE:0.3:dot + +# dataRange (auto-reads headers) with diamond markers +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop dataRange=Sheet1!A1:E13 \ + --prop showMarkers=true --prop marker=diamond:5:333333 \ + --prop legend=bottom --prop legendfont=9:58626E:Calibri + +# Inline data shorthand with marker=none +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop 'data=Actual:80,120,160;Target:100,130,160' \ + --prop marker=none --prop legend=right +``` + +**Features:** `series1=Name:v1,v2`, `series1.name`/`.values`/`.categories` (cell range), `dataRange`, `data` (shorthand), `categories`, `colors`, `catTitle`, `axisTitle`, `axisfont`, `gridlines`, `minorGridlines`, `showMarkers`, `marker` (circle, diamond, none), `legend` (bottom, right), `legendfont` + +### Sheet: 2-Line Styles + +Four charts demonstrating visual styling — smoothing, dash patterns, markers, and transparency. + +```bash +# Smooth curves with shadow, axes hidden +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop smooth=true --prop lineWidth=2.5 \ + --prop gridlines=none --prop axisVisible=false \ + --prop series.shadow=000000-4-315-2-40 + +# Dashed lines (applies to all series) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop lineDash=dash --prop lineWidth=2 + +# Marker styles with series outline +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop showMarkers=true --prop marker=square:7:4472C4 \ + --prop series.outline=FFFFFF-0.5 + +# Transparent lines on gradient plot area +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop lineWidth=3 --prop smooth=true \ + --prop transparency=30 \ + --prop plotFill=F0F4F8-D6E4F0:90 --prop chartFill=FFFFFF \ + --prop title.font=Georgia --prop title.size=14 \ + --prop title.color=1F4E79 --prop title.bold=true \ + --prop roundedCorners=true +``` + +**Features:** `smooth`, `lineWidth`, `lineDash` (solid/dot/dash/dashdot/longdash/longdashdot/longdashdotdot), `marker` (square), `series.shadow` (color-blur-angle-dist-opacity), `series.outline`, `transparency`, `plotFill` (gradient), `chartFill`, `title.font`/`.size`/`.color`/`.bold`, `roundedCorners`, `gridlines=none`, `axisVisible=false` + +### Sheet: 3-Line Variants + +Four charts covering all line chart type variants. + +```bash +# Stacked line — cumulative values +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=lineStacked \ + --prop majorTickMark=outside --prop tickLabelPos=low + +# 100% stacked line — proportional +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=linePercentStacked \ + --prop axisNumFmt=0% + +# 3D line with perspective +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line3d \ + --prop view3d=15,20,30 --prop style=3 + +# Stacked line with data table +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=lineStacked \ + --prop dataTable=true --prop legend=none +``` + +**Features:** `lineStacked`, `linePercentStacked`, `line3d`, `majorTickMark`, `tickLabelPos`, `axisNumFmt`, `view3d` (rotX,rotY,perspective), `style` (preset 1-48), `dataTable`, `legend=none` + +### Sheet: 4-Axis & Gridlines + +Four charts demonstrating every axis and gridline configuration. + +```bash +# Custom axis scaling with axis lines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop axisMin=80 --prop axisMax=220 \ + --prop majorUnit=20 --prop minorUnit=10 \ + --prop axisLine=C00000:1.5:solid --prop catAxisLine=2E75B6:1.5:solid + +# Logarithmic scale +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop logBase=10 \ + --prop marker=triangle:7:C00000 + +# Reversed value axis +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop axisReverse=true + +# Display units with tick marks +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop dispUnits=thousands \ + --prop majorTickMark=outside --prop minorTickMark=inside \ + --prop marker=star:7:2E75B6 +``` + +**Features:** `axisMin`, `axisMax`, `majorUnit`, `minorUnit`, `axisLine`, `catAxisLine`, `logBase` (logarithmic scale), `axisReverse` (flip direction), `dispUnits` (thousands/millions), `majorTickMark`, `minorTickMark`, `marker` (triangle, star) + +### Sheet: 5-Labels & Legend + +Four charts demonstrating data label and legend customization. + +```bash +# Data labels with number format +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop dataLabels=true --prop labelPos=top \ + --prop labelFont=9:333333:true \ + --prop dataLabels.numFmt=#,##0 \ + --prop dataLabels.separator=": " + +# Custom individual data labels (hide some, highlight peak with color + label) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop dataLabels=true \ + --prop dataLabel1.delete=true --prop dataLabel2.delete=true \ + --prop point4.color=C00000 \ + --prop dataLabel4.text="Peak: 210" --prop dataLabel4.y=0.15 + +# Legend overlay +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop legend=top --prop legend.overlay=true \ + --prop legendfont=10:1F4E79:Calibri + +# Manual layout — plotArea, title, legend positioning +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop plotArea.x=0.12 --prop plotArea.y=0.18 \ + --prop plotArea.w=0.82 --prop plotArea.h=0.55 \ + --prop title.x=0.25 --prop title.y=0.02 \ + --prop legend.x=0.15 --prop legend.y=0.82 \ + --prop legend.w=0.7 --prop legend.h=0.12 +``` + +**Features:** `dataLabels`, `labelPos` (top/center/insideEnd/outsideEnd/bestFit), `labelFont`, `dataLabels.numFmt`, `dataLabels.separator`, `dataLabel{N}.delete`, `dataLabel{N}.text`, `dataLabel{N}.y` (manual label position), `point{N}.color` (individual point color), `legend` (top), `legend.overlay`, `legendfont`, `plotArea.x/y/w/h`, `title.x/y`, `legend.x/y/w/h` + +### Sheet: 6-Effects & Advanced + +Four charts demonstrating advanced features — secondary axis, reference lines, effects, and conditional coloring. + +```bash +# Secondary axis (dual scale) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop secondaryAxis=2 \ + --prop series1="Revenue:120,180,250,310" \ + --prop series2="Growth %:50,33,39,24" + +# Reference line with longdash style +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop referenceLine=150:FF0000:1.5:dash \ + --prop lineDash=longdash + +# Title glow/shadow effects +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop title.glow=4472C4-8-60 \ + --prop title.shadow=000000-3-315-2-40 \ + --prop series.shadow=000000-3-315-1-30 + +# Conditional coloring with chart/plot borders +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop colorRule=0:C00000:70AD47 \ + --prop referenceLine=0:888888:1:solid \ + --prop chartArea.border=D0D0D0:1:solid \ + --prop plotArea.border=E0E0E0:0.5:dot +``` + +**Features:** `secondaryAxis` (1-based series indices), `referenceLine` (value:color:width:dash), `title.glow` (color-radius-opacity), `title.shadow` (color-blur-angle-dist-opacity), `series.shadow`, `colorRule` (threshold:belowColor:aboveColor), `chartArea.border`, `plotArea.border` + +### Sheet: 7-Line Elements + +Four charts demonstrating line-chart-specific structural elements. + +```bash +# Drop lines — vertical lines from points to X axis +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop dropLines=true + +# High-low lines — connect highest and lowest series per category +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop hiLowLines=true + +# Up-down bars with custom gain/loss colors +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line \ + --prop updownbars=100:70AD47:C00000 + +# 3D line with gap depth +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=line3d \ + --prop gapDepth=300 +``` + +**Features:** `dropLines` (vertical drop to axis), `hiLowLines` (high-low connectors), `updownbars` (gapWidth:upColor:downColor), `gapDepth` (3D depth spacing 0-500) + +## Complete Feature Coverage + +| Feature | Sheet | +|---------|-------| +| **Chart types:** line, lineStacked, linePercentStacked, line3d | 1, 3 | +| **Data input:** series, dataRange, data, series.name/values/categories | 1 | +| **Line styling:** smooth, lineWidth, lineDash, colors | 2 | +| **Markers:** circle, diamond, square, triangle, star, none, auto | 1, 2, 4 | +| **Axis scaling:** axisMin/Max, majorUnit, minorUnit | 4 | +| **Axis features:** logBase, axisReverse, dispUnits, axisNumFmt | 3, 4 | +| **Axis lines:** axisLine, catAxisLine | 4 | +| **Axis visibility:** axisVisible | 2 | +| **Tick marks:** majorTickMark, minorTickMark, tickLabelPos | 3, 4 | +| **Gridlines:** gridlines, minorGridlines, gridlines=none | 1, 2, 4 | +| **Data labels:** dataLabels, labelPos, labelFont, numFmt, separator | 5 | +| **Custom labels:** dataLabel{N}.text, dataLabel{N}.delete, dataLabel{N}.y | 5 | +| **Point color:** point{N}.color | 5 | +| **Legend:** position, legendfont, legend.overlay, legend=none | 1, 3, 5 | +| **Layout:** plotArea.x/y/w/h, title.x/y, legend.x/y/w/h | 5 | +| **Effects:** series.shadow, series.outline, transparency | 2, 6 | +| **Title styling:** font, size, color, bold, glow, shadow | 2, 6 | +| **Fills:** plotFill, chartFill (solid + gradient) | 2, 3, 6 | +| **Borders:** chartArea.border, plotArea.border | 6 | +| **Advanced:** secondaryAxis, referenceLine, colorRule | 6 | +| **Line elements:** dropLines, hiLowLines, upDownBars | 7 | +| **3D:** view3d, gapDepth, style | 3, 7 | +| **Other:** dataTable, roundedCorners | 2, 3 | + +## Inspect the Generated File + +```bash +officecli query charts-line.xlsx chart +officecli get charts-line.xlsx "/1-Line Fundamentals/chart[1]" +``` diff --git a/examples/excel/charts/charts-line.py b/examples/excel/charts/charts-line.py new file mode 100644 index 000000000..3c267e4ba --- /dev/null +++ b/examples/excel/charts/charts-line.py @@ -0,0 +1,993 @@ +#!/usr/bin/env python3 +""" +Line Charts Showcase — line, lineStacked, linePercentStacked, and line3d with all variations. + +Generates: charts-line.xlsx + +Every line chart feature officecli supports is demonstrated at least once: +line styles, markers, smoothing, dash patterns, axis scaling, gridlines, +data labels, legend positioning, reference lines, secondary axis, error bars, +gradients, transparency, shadows, manual layout, data table, and 3D rotation. + +6 sheets, 24 charts total. + + 1-Line Fundamentals 4 charts — data input variants, markers, cell-range series + 2-Line Styles 4 charts — lineWidth, lineDash, smooth, color palettes + 3-Line Variants 4 charts — lineStacked, linePercentStacked, line3d + 4-Axis & Gridlines 4 charts — axis scaling, log scale, reverse, tick marks + 5-Labels & Legend 4 charts — data labels, custom labels, legend layout + 6-Effects & Advanced 4 charts — shadows, gradients, secondary axis, reference lines + +Usage: + python3 charts-line.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-line.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Source data — shared across all charts +# ========================================================================== +print("\n--- Populating source data ---") + +data_cmds = [] +for j, h in enumerate(["Month", "East", "South", "North", "West"]): + data_cmds.append({"command": "set", "path": f"/Sheet1/{'ABCDE'[j]}1", "props": {"text": h, "bold": "true"}}) + +months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"] +east = [120, 135, 148, 162, 155, 178, 195, 210, 188, 172, 165, 198] +south = [95, 108, 115, 128, 142, 155, 168, 175, 160, 148, 135, 158] +north = [88, 92, 105, 118, 125, 138, 145, 152, 140, 130, 122, 142] +west = [110, 118, 130, 145, 138, 162, 175, 190, 170, 155, 148, 180] + +for i in range(12): + r = i + 2 + for j, val in enumerate([months[i], east[i], south[i], north[i], west[i]]): + data_cmds.append({"command": "set", "path": f"/Sheet1/{'ABCDE'[j]}{r}", "props": {"text": str(val)}}) + +cli(f'batch "{FILE}" --force --commands \'{json.dumps(data_cmds)}\'') + +# ========================================================================== +# Sheet: 1-Line Fundamentals +# ========================================================================== +print("\n--- 1-Line Fundamentals ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Line Fundamentals"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic line with inline named series and categories +# +# officecli add charts-line.xlsx "/1-Line Fundamentals" --type chart \ +# --prop chartType=line \ +# --prop title="Quarterly Revenue" \ +# --prop series1="Product A:120,180,210,250" \ +# --prop series2="Product B:90,140,160,200" \ +# --prop series3="Product C:60,85,110,145" \ +# --prop categories=Q1,Q2,Q3,Q4 \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop catTitle=Quarter --prop axisTitle=Revenue \ +# --prop axisfont=9:C00000:Arial \ +# --prop gridlines=D9D9D9:0.5:dot +# +# Features: chartType=line, inline series (series1=Name:v1,v2,...), +# categories, colors, catTitle, axisTitle, axisfont, gridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Line Fundamentals" --type chart' + f' --prop chartType=line' + f' --prop title="Quarterly Revenue"' + f' --prop series1="Product A:120,180,210,250"' + f' --prop series2="Product B:90,140,160,200"' + f' --prop series3="Product C:60,85,110,145"' + f' --prop categories=Q1,Q2,Q3,Q4' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop catTitle=Quarter --prop axisTitle=Revenue' + f' --prop axisfont=9:C00000:Arial' + f' --prop gridlines=D9D9D9:0.5:dot') + +# -------------------------------------------------------------------------- +# Chart 2: Line with cell-range series (dotted syntax) and markers +# +# officecli add charts-line.xlsx "/1-Line Fundamentals" --type chart \ +# --prop chartType=line \ +# --prop title="East Region Trend" \ +# --prop series1.name=East \ +# --prop series1.values=Sheet1!B2:B13 \ +# --prop series1.categories=Sheet1!A2:A13 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop showMarkers=true --prop marker=circle:6:2E75B6 \ +# --prop gridlines=D9D9D9:0.5:dot \ +# --prop minorGridlines=EEEEEE:0.3:dot +# +# Features: series.name/values/categories (cell range via dotted syntax), +# showMarkers, marker (style:size:color), minorGridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Line Fundamentals" --type chart' + f' --prop chartType=line' + f' --prop title="East Region Trend"' + f' --prop series1.name=East' + f' --prop series1.values=Sheet1!B2:B13' + f' --prop series1.categories=Sheet1!A2:A13' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop showMarkers=true --prop marker=circle:6:2E75B6' + f' --prop gridlines=D9D9D9:0.5:dot' + f' --prop minorGridlines=EEEEEE:0.3:dot') + +# -------------------------------------------------------------------------- +# Chart 3: Line from dataRange with all four regions +# +# officecli add charts-line.xlsx "/1-Line Fundamentals" --type chart \ +# --prop chartType=line \ +# --prop title="All Regions — Full Year" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=2E75B6,70AD47,FFC000,C00000 \ +# --prop showMarkers=true --prop marker=diamond:5:333333 \ +# --prop lineWidth=2 \ +# --prop legend=bottom \ +# --prop legendfont=9:58626E:Calibri +# +# Features: dataRange (auto-reads headers as series names), marker=diamond, +# lineWidth, legend=bottom, legendfont +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Line Fundamentals" --type chart' + f' --prop chartType=line' + f' --prop title="All Regions — Full Year"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=2E75B6,70AD47,FFC000,C00000' + f' --prop showMarkers=true --prop marker=diamond:5:333333' + f' --prop lineWidth=2' + f' --prop legend=bottom' + f' --prop legendfont=9:58626E:Calibri') + +# -------------------------------------------------------------------------- +# Chart 4: Line with inline data shorthand and marker=none +# +# officecli add charts-line.xlsx "/1-Line Fundamentals" --type chart \ +# --prop chartType=line \ +# --prop title="Simple Two-Series" \ +# --prop 'data=Actual:80,120,160,200,240;Target:100,130,160,190,220' \ +# --prop categories=Week 1,Week 2,Week 3,Week 4,Week 5 \ +# --prop colors=0070C0,FF0000 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop marker=none \ +# --prop legend=right +# +# Features: data (inline shorthand Name:v1;Name2:v2), marker=none, +# legend=right +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Line Fundamentals" --type chart' + f' --prop chartType=line' + f' --prop title="Simple Two-Series"' + f' --prop "data=Actual:80,120,160,200,240;Target:100,130,160,190,220"' + f' --prop categories=Week 1,Week 2,Week 3,Week 4,Week 5' + f' --prop colors=0070C0,FF0000' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop marker=none' + f' --prop legend=right') + +# ========================================================================== +# Sheet: 2-Line Styles +# ========================================================================== +print("\n--- 2-Line Styles ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Line Styles"') + +# -------------------------------------------------------------------------- +# Chart 1: Smooth line with thick width and shadow +# +# officecli add charts-line.xlsx "/2-Line Styles" --type chart \ +# --prop chartType=line \ +# --prop title="Smooth Curves with Shadow" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop smooth=true --prop lineWidth=2.5 \ +# --prop colors=0070C0,00B050,FFC000,FF0000 \ +# --prop gridlines=none \ +# --prop axisVisible=false \ +# --prop series.shadow=000000-4-315-2-40 +# +# Features: smooth=true (Bezier curves), lineWidth=2.5, gridlines=none, +# axisVisible=false (hide both axes for sparkline-like minimal look), +# series.shadow (color-blur-angle-dist-opacity) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Line Styles" --type chart' + f' --prop chartType=line' + f' --prop title="Smooth Curves with Shadow"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop smooth=true --prop lineWidth=2.5' + f' --prop colors=0070C0,00B050,FFC000,FF0000' + f' --prop gridlines=none' + f' --prop axisVisible=false' + f' --prop series.shadow=000000-4-315-2-40') + +# -------------------------------------------------------------------------- +# Chart 2: Dashed lines — all dash styles demonstrated +# +# officecli add charts-line.xlsx "/2-Line Styles" --type chart \ +# --prop chartType=line \ +# --prop title="Dash Pattern Gallery" \ +# --prop series1="solid:120,135,148,162,155" \ +# --prop series2="dot:95,108,115,128,142" \ +# --prop series3="dash:88,92,105,118,125" \ +# --prop series4="dashdot:110,118,130,145,138" \ +# --prop categories=Jan,Feb,Mar,Apr,May \ +# --prop colors=2E75B6,ED7D31,70AD47,FFC000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop lineDash=dash --prop lineWidth=2 \ +# --prop legend=bottom +# +# Note: lineDash applies to ALL series. Supported values: +# solid, dot, dash, dashdot, longdash, longdashdot, longdashdotdot +# +# Features: lineDash (applied globally to all series), lineWidth +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Line Styles" --type chart' + f' --prop chartType=line' + f' --prop title="Dash Pattern Gallery"' + f' --prop series1="solid:120,135,148,162,155"' + f' --prop series2="dot:95,108,115,128,142"' + f' --prop series3="dash:88,92,105,118,125"' + f' --prop series4="dashdot:110,118,130,145,138"' + f' --prop categories=Jan,Feb,Mar,Apr,May' + f' --prop colors=2E75B6,ED7D31,70AD47,FFC000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop lineDash=dash --prop lineWidth=2' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Multiple marker styles — circle, square, triangle, star +# +# officecli add charts-line.xlsx "/2-Line Styles" --type chart \ +# --prop chartType=line \ +# --prop title="Marker Style Showcase" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop showMarkers=true --prop marker=square:7:4472C4 \ +# --prop lineWidth=1.5 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop series.outline=FFFFFF-0.5 +# +# Note: marker applies to ALL series. Supported styles: +# circle, diamond, square, triangle, star, x, plus, dash, dot, none +# +# Features: marker=square:7:color (style:size:fillColor), +# series.outline (white border around markers/lines) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Line Styles" --type chart' + f' --prop chartType=line' + f' --prop title="Marker Style Showcase"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop showMarkers=true --prop marker=square:7:4472C4' + f' --prop lineWidth=1.5' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop series.outline=FFFFFF-0.5') + +# -------------------------------------------------------------------------- +# Chart 4: Transparent lines with gradient plot area and styled title +# +# officecli add charts-line.xlsx "/2-Line Styles" --type chart \ +# --prop chartType=line \ +# --prop title="Translucent Lines on Gradient" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop lineWidth=3 --prop smooth=true \ +# --prop transparency=30 \ +# --prop plotFill=F0F4F8-D6E4F0:90 \ +# --prop chartFill=FFFFFF \ +# --prop colors=1F4E79,2E75B6,5B9BD5,9DC3E6 \ +# --prop title.font=Georgia --prop title.size=14 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop roundedCorners=true +# +# Features: transparency=30 (30% transparent), plotFill gradient, +# chartFill, title.font/size/color/bold, roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Line Styles" --type chart' + f' --prop chartType=line' + f' --prop title="Translucent Lines on Gradient"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop lineWidth=3 --prop smooth=true' + f' --prop transparency=30' + f' --prop plotFill=F0F4F8-D6E4F0:90' + f' --prop chartFill=FFFFFF' + f' --prop colors=1F4E79,2E75B6,5B9BD5,9DC3E6' + f' --prop title.font=Georgia --prop title.size=14' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop roundedCorners=true') + +# ========================================================================== +# Sheet: 3-Line Variants +# ========================================================================== +print("\n--- 3-Line Variants ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Line Variants"') + +# -------------------------------------------------------------------------- +# Chart 1: Stacked line chart +# +# officecli add charts-line.xlsx "/3-Line Variants" --type chart \ +# --prop chartType=lineStacked \ +# --prop title="Cumulative Sales by Region" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop catTitle=Month --prop axisTitle=Cumulative \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop majorTickMark=outside --prop tickLabelPos=low +# +# Features: lineStacked (cumulative stacking), majorTickMark=outside, +# tickLabelPos=low +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Line Variants" --type chart' + f' --prop chartType=lineStacked' + f' --prop title="Cumulative Sales by Region"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop catTitle=Month --prop axisTitle=Cumulative' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop majorTickMark=outside --prop tickLabelPos=low') + +# -------------------------------------------------------------------------- +# Chart 2: 100% stacked line chart with axis number format +# +# officecli add charts-line.xlsx "/3-Line Variants" --type chart \ +# --prop chartType=linePercentStacked \ +# --prop title="Regional Contribution %" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=1F4E79,2E75B6,9DC3E6,BDD7EE \ +# --prop axisNumFmt=0% \ +# --prop legend=right \ +# --prop gridlines=E0E0E0:0.5:solid +# +# Features: linePercentStacked (each month sums to 100%), +# axisNumFmt (value axis number format) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Line Variants" --type chart' + f' --prop chartType=linePercentStacked' + f' --prop title="Regional Contribution %"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=1F4E79,2E75B6,9DC3E6,BDD7EE' + f' --prop axisNumFmt=0%' + f' --prop legend=right' + f' --prop gridlines=E0E0E0:0.5:solid') + +# -------------------------------------------------------------------------- +# Chart 3: 3D line chart with perspective +# +# officecli add charts-line.xlsx "/3-Line Variants" --type chart \ +# --prop chartType=line3d \ +# --prop title="3D Regional Trends" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=15,20,30 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop chartFill=F8F8F8 \ +# --prop style=3 +# +# Features: line3d (3D line chart), view3d (rotX,rotY,perspective), +# style/styleId (preset chart style 1-48) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Line Variants" --type chart' + f' --prop chartType=line3d' + f' --prop title="3D Regional Trends"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=15,20,30' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop chartFill=F8F8F8' + f' --prop style=3') + +# -------------------------------------------------------------------------- +# Chart 4: Stacked line with area fill and data table +# +# officecli add charts-line.xlsx "/3-Line Variants" --type chart \ +# --prop chartType=lineStacked \ +# --prop title="Stacked with Data Table" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop dataTable=true \ +# --prop legend=none \ +# --prop lineWidth=1.5 \ +# --prop colors=2E75B6,ED7D31,70AD47,FFC000 \ +# --prop plotFill=FAFAFA +# +# Features: dataTable=true (show value table below chart), +# legend=none (hidden because data table shows series names) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Line Variants" --type chart' + f' --prop chartType=lineStacked' + f' --prop title="Stacked with Data Table"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop dataTable=true' + f' --prop legend=none' + f' --prop lineWidth=1.5' + f' --prop colors=2E75B6,ED7D31,70AD47,FFC000' + f' --prop plotFill=FAFAFA') + +# ========================================================================== +# Sheet: 4-Axis & Gridlines +# ========================================================================== +print("\n--- 4-Axis & Gridlines ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Axis & Gridlines"') + +# -------------------------------------------------------------------------- +# Chart 1: Custom axis scaling — min, max, majorUnit +# +# officecli add charts-line.xlsx "/4-Axis & Gridlines" --type chart \ +# --prop chartType=line \ +# --prop title="Custom Axis Scale (80–220)" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop axisMin=80 --prop axisMax=220 --prop majorUnit=20 \ +# --prop minorUnit=10 \ +# --prop showMarkers=true --prop marker=circle:4:4472C4 \ +# --prop gridlines=D0D0D0:0.5:solid \ +# --prop minorGridlines=EEEEEE:0.3:dot \ +# --prop axisLine=C00000:1.5:solid \ +# --prop catAxisLine=2E75B6:1.5:solid +# +# Features: axisMin, axisMax, majorUnit, minorUnit, +# axisLine (value axis line styling — red), catAxisLine (category axis line — blue) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Gridlines" --type chart' + f' --prop chartType=line' + f' --prop title="Custom Axis Scale (80–220)"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop axisMin=80 --prop axisMax=220 --prop majorUnit=20' + f' --prop minorUnit=10' + f' --prop showMarkers=true --prop marker=circle:4:4472C4' + f' --prop gridlines=D0D0D0:0.5:solid' + f' --prop minorGridlines=EEEEEE:0.3:dot' + f' --prop axisLine=C00000:1.5:solid' + f' --prop catAxisLine=2E75B6:1.5:solid') + +# -------------------------------------------------------------------------- +# Chart 2: Logarithmic scale with display units +# +# officecli add charts-line.xlsx "/4-Axis & Gridlines" --type chart \ +# --prop chartType=line \ +# --prop title="Exponential Growth (Log Scale)" \ +# --prop series1="Growth:1,5,25,125,625,3125" \ +# --prop categories=Year 1,Year 2,Year 3,Year 4,Year 5,Year 6 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop logBase=10 \ +# --prop colors=C00000 \ +# --prop lineWidth=2.5 \ +# --prop showMarkers=true --prop marker=triangle:7:C00000 \ +# --prop axisTitle=Value (log₁₀) \ +# --prop catTitle=Year \ +# --prop gridlines=E0E0E0:0.5:dash +# +# Features: logBase=10 (logarithmic scale), marker=triangle +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Gridlines" --type chart' + f' --prop chartType=line' + f' --prop title="Exponential Growth (Log Scale)"' + f' --prop "series1=Growth:1,5,25,125,625,3125"' + f' --prop "categories=Year 1,Year 2,Year 3,Year 4,Year 5,Year 6"' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop logBase=10' + f' --prop colors=C00000' + f' --prop lineWidth=2.5' + f' --prop showMarkers=true --prop marker=triangle:7:C00000' + f' --prop axisTitle="Value (log)"' + f' --prop catTitle=Year' + f' --prop gridlines=E0E0E0:0.5:dash') + +# -------------------------------------------------------------------------- +# Chart 3: Reversed axis and hidden axes +# +# officecli add charts-line.xlsx "/4-Axis & Gridlines" --type chart \ +# --prop chartType=line \ +# --prop title="Reversed Value Axis" \ +# --prop series1="Depth:0,50,120,200,350,500" \ +# --prop categories=Station A,Station B,Station C,Station D,Station E,Station F \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop axisReverse=true \ +# --prop colors=0070C0 \ +# --prop lineWidth=2 \ +# --prop showMarkers=true --prop marker=diamond:6:0070C0 \ +# --prop smooth=true \ +# --prop axisTitle="Depth (m)" \ +# --prop gridlines=D9D9D9:0.5:solid +# +# Features: axisReverse=true (value axis direction flipped), +# smooth + markers together +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Gridlines" --type chart' + f' --prop chartType=line' + f' --prop title="Reversed Value Axis"' + f' --prop "series1=Depth:0,50,120,200,350,500"' + f' --prop "categories=Station A,Station B,Station C,Station D,Station E,Station F"' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop axisReverse=true' + f' --prop colors=0070C0' + f' --prop lineWidth=2' + f' --prop showMarkers=true --prop marker=diamond:6:0070C0' + f' --prop smooth=true' + f' --prop axisTitle="Depth (m)"' + f' --prop gridlines=D9D9D9:0.5:solid') + +# -------------------------------------------------------------------------- +# Chart 4: Display units and tick mark styles +# +# officecli add charts-line.xlsx "/4-Axis & Gridlines" --type chart \ +# --prop chartType=line \ +# --prop title="Revenue (in Thousands)" \ +# --prop series1="Revenue:12000,18500,22000,31000,45000,52000" \ +# --prop series2="Cost:8000,11000,14000,19500,28000,33000" \ +# --prop categories=2020,2021,2022,2023,2024,2025 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop dispUnits=thousands \ +# --prop colors=2E75B6,C00000 \ +# --prop lineWidth=2 \ +# --prop majorTickMark=outside --prop minorTickMark=inside \ +# --prop showMarkers=true --prop marker=star:7:2E75B6 \ +# --prop catTitle=Year --prop axisTitle=Amount (K) +# +# Features: dispUnits=thousands (display units label), +# majorTickMark=outside, minorTickMark=inside, marker=star +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Axis & Gridlines" --type chart' + f' --prop chartType=line' + f' --prop title="Revenue (in Thousands)"' + f' --prop "series1=Revenue:12000,18500,22000,31000,45000,52000"' + f' --prop "series2=Cost:8000,11000,14000,19500,28000,33000"' + f' --prop categories=2020,2021,2022,2023,2024,2025' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop dispUnits=thousands' + f' --prop colors=2E75B6,C00000' + f' --prop lineWidth=2' + f' --prop majorTickMark=outside --prop minorTickMark=inside' + f' --prop showMarkers=true --prop marker=star:7:2E75B6' + f' --prop catTitle=Year --prop axisTitle="Amount (K)"') + +# ========================================================================== +# Sheet: 5-Labels & Legend +# ========================================================================== +print("\n--- 5-Labels & Legend ---") +cli(f'add "{FILE}" / --type sheet --prop name="5-Labels & Legend"') + +# -------------------------------------------------------------------------- +# Chart 1: Data labels at various positions with number format +# +# officecli add charts-line.xlsx "/5-Labels & Legend" --type chart \ +# --prop chartType=line \ +# --prop title="Sales with Labels" \ +# --prop series1="Revenue:120,180,210,250,280" \ +# --prop categories=Jan,Feb,Mar,Apr,May \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=4472C4 \ +# --prop lineWidth=2 \ +# --prop showMarkers=true --prop marker=circle:6:4472C4 \ +# --prop dataLabels=true --prop labelPos=top \ +# --prop labelFont=9:333333:true \ +# --prop dataLabels.numFmt=#,##0 \ +# --prop dataLabels.separator=": " +# +# Features: dataLabels=true, labelPos=top, labelFont (size:color:bold), +# dataLabels.numFmt (number format), dataLabels.separator +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Labels & Legend" --type chart' + f' --prop chartType=line' + f' --prop title="Sales with Labels"' + f' --prop "series1=Revenue:120,180,210,250,280"' + f' --prop categories=Jan,Feb,Mar,Apr,May' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=4472C4' + f' --prop lineWidth=2' + f' --prop showMarkers=true --prop marker=circle:6:4472C4' + f' --prop dataLabels=true --prop labelPos=top' + f' --prop labelFont=9:333333:true' + f' --prop dataLabels.numFmt=#,##0' + f' --prop "dataLabels.separator=: "') + +# -------------------------------------------------------------------------- +# Chart 2: Custom individual data labels (highlight peak) +# +# officecli add charts-line.xlsx "/5-Labels & Legend" --type chart \ +# --prop chartType=line \ +# --prop title="Peak Highlight" \ +# --prop series1="Sales:88,120,165,210,195,178" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=2E75B6 \ +# --prop lineWidth=2.5 --prop smooth=true \ +# --prop showMarkers=true --prop marker=circle:5:2E75B6 \ +# --prop dataLabels=true --prop labelPos=top \ +# --prop dataLabel1.delete=true --prop dataLabel2.delete=true \ +# --prop point4.color=C00000 \ +# --prop dataLabel4.text="Peak: 210" \ +# --prop dataLabel4.y=0.15 \ +# --prop dataLabel5.delete=true --prop dataLabel6.delete=true +# +# Features: dataLabel{N}.delete (hide specific labels), +# dataLabel{N}.text (custom text on specific point), +# point{N}.color (highlight individual data point marker in red), +# dataLabel{N}.y (manual vertical position of individual label, 0-1 fraction) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Labels & Legend" --type chart' + f' --prop chartType=line' + f' --prop title="Peak Highlight"' + f' --prop "series1=Sales:88,120,165,210,195,178"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=2E75B6' + f' --prop lineWidth=2.5 --prop smooth=true' + f' --prop showMarkers=true --prop marker=circle:5:2E75B6' + f' --prop dataLabels=true --prop labelPos=top' + f' --prop dataLabel1.delete=true --prop dataLabel2.delete=true' + f' --prop point4.color=C00000' + f' --prop dataLabel4.text="Peak: 210"' + f' --prop dataLabel4.y=0.15' + f' --prop dataLabel5.delete=true --prop dataLabel6.delete=true') + +# -------------------------------------------------------------------------- +# Chart 3: Legend positioning and overlay +# +# officecli add charts-line.xlsx "/5-Labels & Legend" --type chart \ +# --prop chartType=line \ +# --prop title="Legend Overlay on Chart" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop lineWidth=2 \ +# --prop legend=top \ +# --prop legend.overlay=true \ +# --prop legendfont=10:1F4E79:Calibri \ +# --prop plotFill=F5F5F5 +# +# Features: legend=top, legend.overlay=true (legend overlays chart area), +# legendfont (size:color:fontname) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Labels & Legend" --type chart' + f' --prop chartType=line' + f' --prop title="Legend Overlay on Chart"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop lineWidth=2' + f' --prop legend=top' + f' --prop legend.overlay=true' + f' --prop legendfont=10:1F4E79:Calibri' + f' --prop plotFill=F5F5F5') + +# -------------------------------------------------------------------------- +# Chart 4: Manual layout — plotArea, title, and legend positioning +# +# officecli add charts-line.xlsx "/5-Labels & Legend" --type chart \ +# --prop chartType=line \ +# --prop title="Manual Layout Control" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=2E75B6,ED7D31,70AD47,FFC000 \ +# --prop lineWidth=1.5 \ +# --prop plotArea.x=0.12 --prop plotArea.y=0.18 \ +# --prop plotArea.w=0.82 --prop plotArea.h=0.55 \ +# --prop title.x=0.25 --prop title.y=0.02 \ +# --prop legend.x=0.15 --prop legend.y=0.82 \ +# --prop legend.w=0.7 --prop legend.h=0.12 \ +# --prop title.font=Arial --prop title.size=13 \ +# --prop title.bold=true +# +# Features: plotArea.x/y/w/h (plot area manual layout, 0-1 fraction), +# title.x/y (title position), legend.x/y/w/h (legend position/size) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Labels & Legend" --type chart' + f' --prop chartType=line' + f' --prop title="Manual Layout Control"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=2E75B6,ED7D31,70AD47,FFC000' + f' --prop lineWidth=1.5' + f' --prop plotArea.x=0.12 --prop plotArea.y=0.18' + f' --prop plotArea.w=0.82 --prop plotArea.h=0.55' + f' --prop title.x=0.25 --prop title.y=0.02' + f' --prop legend.x=0.15 --prop legend.y=0.82' + f' --prop legend.w=0.7 --prop legend.h=0.12' + f' --prop title.font=Arial --prop title.size=13' + f' --prop title.bold=true') + +# ========================================================================== +# Sheet: 6-Effects & Advanced +# ========================================================================== +print("\n--- 6-Effects & Advanced ---") +cli(f'add "{FILE}" / --type sheet --prop name="6-Effects & Advanced"') + +# -------------------------------------------------------------------------- +# Chart 1: Secondary axis — two series on different scales +# +# officecli add charts-line.xlsx "/6-Effects & Advanced" --type chart \ +# --prop chartType=line \ +# --prop title="Revenue vs Growth Rate" \ +# --prop series1="Revenue:120,180,250,310,380,420" \ +# --prop series2="Growth %:50,33,39,24,23,11" \ +# --prop categories=2020,2021,2022,2023,2024,2025 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop secondaryAxis=2 \ +# --prop colors=2E75B6,C00000 \ +# --prop lineWidth=2.5 \ +# --prop showMarkers=true --prop marker=circle:6:2E75B6 \ +# --prop catTitle=Year --prop axisTitle=Revenue \ +# --prop dataLabels=true --prop labelPos=top +# +# Features: secondaryAxis=2 (series 2 on right-hand axis), +# dual-scale visualization +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Effects & Advanced" --type chart' + f' --prop chartType=line' + f' --prop title="Revenue vs Growth Rate"' + f' --prop "series1=Revenue:120,180,250,310,380,420"' + f' --prop "series2=Growth %:50,33,39,24,23,11"' + f' --prop categories=2020,2021,2022,2023,2024,2025' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop secondaryAxis=2' + f' --prop colors=2E75B6,C00000' + f' --prop lineWidth=2.5' + f' --prop showMarkers=true --prop marker=circle:6:2E75B6' + f' --prop catTitle=Year --prop axisTitle=Revenue' + f' --prop dataLabels=true --prop labelPos=top') + +# -------------------------------------------------------------------------- +# Chart 2: Reference line (target/threshold) with error bars +# +# officecli add charts-line.xlsx "/6-Effects & Advanced" --type chart \ +# --prop chartType=line \ +# --prop title="vs Target (150)" \ +# --prop dataRange=Sheet1!A1:C13 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,70AD47 \ +# --prop lineWidth=2 \ +# --prop referenceLine=150:FF0000:1.5:dash \ +# --prop showMarkers=true --prop marker=circle:4:4472C4 \ +# --prop legend=bottom \ +# --prop lineDash=longdash --prop lineWidth=1.5 +# +# referenceLine format: value:color:width:dash +# - value: the threshold/target value on the Y axis +# - color: hex RGB (no #) +# - width: line thickness in pt (default 1.5) +# - dash: solid/dot/dash/dashdot/longdash +# +# Features: referenceLine (horizontal target line), lineDash=longdash +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Effects & Advanced" --type chart' + f' --prop chartType=line' + f' --prop title="vs Target (150)"' + f' --prop dataRange=Sheet1!A1:C13' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=4472C4,70AD47' + f' --prop lineWidth=2' + f' --prop referenceLine=150:FF0000:1.5:dash' + f' --prop showMarkers=true --prop marker=circle:4:4472C4' + f' --prop legend=bottom' + f' --prop lineDash=longdash --prop lineWidth=1.5') + +# -------------------------------------------------------------------------- +# Chart 3: Title glow/shadow effects with per-series gradients +# +# officecli add charts-line.xlsx "/6-Effects & Advanced" --type chart \ +# --prop chartType=line \ +# --prop title="Glow & Shadow Effects" \ +# --prop series1="East:120,135,148,162,155,178" \ +# --prop series2="West:110,118,130,145,138,162" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop lineWidth=3 --prop smooth=true \ +# --prop colors=4472C4,ED7D31 \ +# --prop title.glow=4472C4-8-60 \ +# --prop title.shadow=000000-3-315-2-40 \ +# --prop title.font=Calibri --prop title.size=16 \ +# --prop title.bold=true --prop title.color=1F4E79 \ +# --prop series.shadow=000000-3-315-1-30 \ +# --prop plotFill=F0F4F8 --prop chartFill=FFFFFF +# +# Features: title.glow (color-radius-opacity), title.shadow, +# series.shadow on line charts, plotFill + chartFill +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Effects & Advanced" --type chart' + f' --prop chartType=line' + f' --prop title="Glow & Shadow Effects"' + f' --prop "series1=East:120,135,148,162,155,178"' + f' --prop "series2=West:110,118,130,145,138,162"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop lineWidth=3 --prop smooth=true' + f' --prop colors=4472C4,ED7D31' + f' --prop title.glow=4472C4-8-60' + f' --prop title.shadow=000000-3-315-2-40' + f' --prop title.font=Calibri --prop title.size=16' + f' --prop title.bold=true --prop title.color=1F4E79' + f' --prop series.shadow=000000-3-315-1-30' + f' --prop plotFill=F0F4F8 --prop chartFill=FFFFFF') + +# -------------------------------------------------------------------------- +# Chart 4: Conditional coloring with chart/plot borders +# +# officecli add charts-line.xlsx "/6-Effects & Advanced" --type chart \ +# --prop chartType=line \ +# --prop title="Conditional Colors & Borders" \ +# --prop series1="Profit:80,120,-30,160,-50,200,140,-20,180,90" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=2E75B6 \ +# --prop lineWidth=2 \ +# --prop showMarkers=true --prop marker=circle:6:2E75B6 \ +# --prop colorRule=0:C00000:70AD47 \ +# --prop referenceLine=0:888888:1:solid \ +# --prop chartArea.border=D0D0D0:1:solid \ +# --prop plotArea.border=E0E0E0:0.5:dot \ +# --prop dataLabels=true --prop labelPos=top \ +# --prop labelFont=8:666666:false +# +# colorRule format: threshold:belowColor:aboveColor +# - values below 0 → red (C00000), above 0 → green (70AD47) +# +# Features: colorRule (threshold-based conditional coloring), +# chartArea.border, plotArea.border, referenceLine=0 (zero line) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Effects & Advanced" --type chart' + f' --prop chartType=line' + f' --prop title="Conditional Colors & Borders"' + f' --prop "series1=Profit:80,120,-30,160,-50,200,140,-20,180,90"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=2E75B6' + f' --prop lineWidth=2' + f' --prop showMarkers=true --prop marker=circle:6:2E75B6' + f' --prop colorRule=0:C00000:70AD47' + f' --prop referenceLine=0:888888:1:solid' + f' --prop chartArea.border=D0D0D0:1:solid' + f' --prop plotArea.border=E0E0E0:0.5:dot' + f' --prop dataLabels=true --prop labelPos=top' + f' --prop labelFont=8:666666:false') + +# ========================================================================== +# Sheet: 7-Line Elements +# ========================================================================== +print("\n--- 7-Line Elements ---") +cli(f'add "{FILE}" / --type sheet --prop name="7-Line Elements"') + +# -------------------------------------------------------------------------- +# Chart 1: Drop lines — vertical lines from data points to category axis +# +# officecli add charts-line.xlsx "/7-Line Elements" --type chart \ +# --prop chartType=line \ +# --prop title="Drop Lines" \ +# --prop dataRange=Sheet1!A1:C13 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,ED7D31 \ +# --prop showMarkers=true --prop marker=circle:5:4472C4 \ +# --prop dropLines=true \ +# --prop legend=bottom +# +# Features: dropLines=true (simple toggle — default thin gray lines) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Line Elements" --type chart' + f' --prop chartType=line' + f' --prop title="Drop Lines"' + f' --prop dataRange=Sheet1!A1:C13' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=4472C4,ED7D31' + f' --prop showMarkers=true --prop marker=circle:5:4472C4' + f' --prop dropLines=true' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: High-low lines — connect highest and lowest series at each point +# +# officecli add charts-line.xlsx "/7-Line Elements" --type chart \ +# --prop chartType=line \ +# --prop title="High-Low Lines" \ +# --prop series1="High:210,195,220,240,230,250" \ +# --prop series2="Low:150,135,160,170,155,180" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop colors=2E75B6,C00000 \ +# --prop showMarkers=true --prop marker=diamond:5:2E75B6 \ +# --prop hiLowLines=true \ +# --prop legend=bottom +# +# Features: hiLowLines=true (lines connecting highest and lowest values) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Line Elements" --type chart' + f' --prop chartType=line' + f' --prop title="High-Low Lines"' + f' --prop "series1=High:210,195,220,240,230,250"' + f' --prop "series2=Low:150,135,160,170,155,180"' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop colors=2E75B6,C00000' + f' --prop showMarkers=true --prop marker=diamond:5:2E75B6' + f' --prop hiLowLines=true' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Up-down bars with custom colors — show gain/loss between series +# +# officecli add charts-line.xlsx "/7-Line Elements" --type chart \ +# --prop chartType=line \ +# --prop title="Up-Down Bars (Gain/Loss)" \ +# --prop series1="Open:120,135,148,130,155,162" \ +# --prop series2="Close:135,128,162,145,170,155" \ +# --prop categories=Mon,Tue,Wed,Thu,Fri,Sat \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop colors=4472C4,ED7D31 \ +# --prop showMarkers=true --prop marker=circle:4:4472C4 \ +# --prop updownbars=100:70AD47:C00000 \ +# --prop legend=bottom +# +# updownbars format: gapWidth:upColor:downColor +# - gapWidth: gap between bars (0-500, default 150) +# - upColor: fill color for increase (Close > Open) +# - downColor: fill color for decrease (Close < Open) +# +# Features: updownbars with custom colors (gain=green, loss=red) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Line Elements" --type chart' + f' --prop chartType=line' + f' --prop title="Up-Down Bars (Gain/Loss)"' + f' --prop "series1=Open:120,135,148,130,155,162"' + f' --prop "series2=Close:135,128,162,145,170,155"' + f' --prop categories=Mon,Tue,Wed,Thu,Fri,Sat' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop colors=4472C4,ED7D31' + f' --prop showMarkers=true --prop marker=circle:4:4472C4' + f' --prop updownbars=100:70AD47:C00000' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Auto markers + 3D line with gapDepth +# +# officecli add charts-line.xlsx "/7-Line Elements" --type chart \ +# --prop chartType=line3d \ +# --prop title="3D Line with Gap Depth" \ +# --prop dataRange=Sheet1!A1:E13 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=15,25,30 \ +# --prop gapDepth=300 \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop chartFill=F5F5F5 +# +# Features: gapDepth=300 (3D depth spacing, 0-500), +# line3d with custom perspective +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/7-Line Elements" --type chart' + f' --prop chartType=line3d' + f' --prop title="3D Line with Gap Depth"' + f' --prop dataRange=Sheet1!A1:E13' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=15,25,30' + f' --prop gapDepth=300' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop chartFill=F5F5F5') + +print(f"\nDone! Generated: {FILE}") +print(" 8 sheets (Sheet1 data + 7 chart sheets, 28 charts total)") diff --git a/examples/excel/charts/charts-line.xlsx b/examples/excel/charts/charts-line.xlsx new file mode 100644 index 000000000..816e5bbe6 Binary files /dev/null and b/examples/excel/charts/charts-line.xlsx differ diff --git a/examples/excel/charts/charts-pie.md b/examples/excel/charts/charts-pie.md new file mode 100644 index 000000000..59a3c40f1 --- /dev/null +++ b/examples/excel/charts/charts-pie.md @@ -0,0 +1,95 @@ +# Pie & Doughnut Charts Showcase + +This demo consists of three files that work together: + +- **charts-pie.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-pie.xlsx** — The generated workbook with 3 sheets (1 default + 2 chart sheets, 8 charts total). +- **charts-pie.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-pie.py +# → charts-pie.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Pie Charts + +Four pie chart variants covering flat, 3D, exploded, and gradient fills. + +```bash +# Basic pie with colors and data labels +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=pie \ + --prop series1="Share:40,25,20,15" \ + --prop categories=Product A,Product B,Product C,Product D \ + --prop colors=4472C4,ED7D31,70AD47,FFC000 \ + --prop dataLabels=true --prop labelPos=outsideEnd + +# Exploded pie with per-point colors and percentage labels +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=pie \ + --prop explosion=15 \ + --prop point1.color=1F4E79 --prop point2.color=2E75B6 \ + --prop dataLabels.numFmt=0.0"%" --prop labelPos=bestFit + +# 3D pie with tilt angle and styled title +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=pie3d \ + --prop view3d=30,0,0 \ + --prop title.font=Georgia --prop title.size=16 \ + --prop labelFont=12:FFFFFF:true --prop labelPos=center + +# Pie with per-slice gradients and leader lines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=pie \ + --prop 'gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90;...' \ + --prop dataLabels.showLeaderLines=true \ + --prop legend=right --prop legendfont=10:333333:Helvetica +``` + +**Features:** `pie`, `pie3d`, `explosion`, `point{N}.color`, `view3d`, `labelPos=bestFit`, `dataLabels.numFmt`, `labelFont`, `title.font/size/color/bold`, `gradients` (per-slice), `dataLabels.showLeaderLines`, `legendfont`, `chartFill`, `roundedCorners` + +### Sheet: 2-Doughnut Charts + +Four doughnut chart variants including multi-ring and styled effects. + +```bash +# Basic doughnut with center labels +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=doughnut \ + --prop dataLabels=true --prop labelPos=center \ + --prop labelFont=14:FFFFFF:true + +# Multi-ring doughnut (multiple series = concentric rings) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=doughnut \ + --prop series1="2024:40,35,25" \ + --prop series2="2025:45,30,25" \ + --prop series.outline=FFFFFF-1 + +# Styled doughnut with shadow effects +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=doughnut \ + --prop series.shadow=000000-4-315-2-30 \ + --prop title.shadow=000000-3-315-2-30 \ + --prop plotFill=F5F5F5 + +# Doughnut with explosion and per-slice gradients +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=doughnut \ + --prop explosion=8 \ + --prop 'gradients=1F4E79-5B9BD5:90;C55A11-F4B183:90;...' +``` + +**Features:** `doughnut`, multi-ring (multiple `series`), `labelPos=center`, `labelFont`, `series.outline`, `series.shadow`, `title.shadow`, `plotFill`, `explosion`, `gradients` + +## Inspect the Generated File + +```bash +officecli query charts-pie.xlsx chart +officecli get charts-pie.xlsx "/1-Pie Charts/chart[1]" +``` diff --git a/examples/excel/charts/charts-pie.py b/examples/excel/charts/charts-pie.py new file mode 100644 index 000000000..d1453a526 --- /dev/null +++ b/examples/excel/charts/charts-pie.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +""" +Pie & Doughnut Charts Showcase — pie, pie3d, and doughnut with all variations. + +Generates: charts-pie.xlsx + +Usage: + python3 charts-pie.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-pie.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Sheet: 1-Pie Charts +# ========================================================================== +print("\n--- 1-Pie Charts ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Pie Charts"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic pie chart with inline data and custom colors +# +# officecli add charts-pie.xlsx "/1-Pie Charts" --type chart \ +# --prop chartType=pie \ +# --prop title="Market Share" \ +# --prop series1="Share:40,25,20,15" \ +# --prop categories=Product A,Product B,Product C,Product D \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=outsideEnd +# +# Features: chartType=pie, inline series, categories, colors, dataLabels +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Pie Charts" --type chart' + f' --prop chartType=pie' + f' --prop title="Market Share"' + f' --prop series1=Share:40,25,20,15' + f' --prop categories=Product A,Product B,Product C,Product D' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelPos=outsideEnd') + +# -------------------------------------------------------------------------- +# Chart 2: Pie with exploded slice and per-point colors +# +# officecli add charts-pie.xlsx "/1-Pie Charts" --type chart \ +# --prop chartType=pie \ +# --prop title="Revenue by Region" \ +# --prop series1="Revenue:35,28,22,15" \ +# --prop categories=North,South,East,West \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop explosion=15 \ +# --prop point1.color=1F4E79 --prop point2.color=2E75B6 \ +# --prop point3.color=9DC3E6 --prop point4.color=BDD7EE \ +# --prop dataLabels=percent --prop labelPos=bestFit +# +# Features: explosion (slice separation %), point{N}.color, labelPos=bestFit, +# dataLabels=percent +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Pie Charts" --type chart' + f' --prop chartType=pie' + f' --prop title="Revenue by Region"' + f' --prop series1=Revenue:35,28,22,15' + f' --prop categories=North,South,East,West' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop explosion=15' + f' --prop point1.color=1F4E79 --prop point2.color=2E75B6' + f' --prop point3.color=9DC3E6 --prop point4.color=BDD7EE' + f' --prop dataLabels=true --prop labelPos=bestFit' + f' --prop dataLabels=percent --prop labelPos=bestFit') + +# -------------------------------------------------------------------------- +# Chart 3: 3D pie with perspective and title styling +# +# officecli add charts-pie.xlsx "/1-Pie Charts" --type chart \ +# --prop chartType=pie3d \ +# --prop title="3D Category Split" \ +# --prop series1="Sales:45,30,25" \ +# --prop categories=Electronics,Clothing,Food \ +# --prop colors=2E75B6,70AD47,FFC000 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop view3d=30,0,0 \ +# --prop title.font=Georgia --prop title.size=16 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop dataLabels=true --prop labelPos=center \ +# --prop labelFont=12:FFFFFF:true +# +# Features: pie3d, view3d on pie (tilt angle), title.font/size/color/bold, +# labelFont (size:color:bold) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Pie Charts" --type chart' + f' --prop chartType=pie3d' + f' --prop title="3D Category Split"' + f' --prop series1=Sales:45,30,25' + f' --prop categories=Electronics,Clothing,Food' + f' --prop colors=2E75B6,70AD47,FFC000' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop view3d=30,0,0' + f' --prop title.font=Georgia --prop title.size=16' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop dataLabels=true --prop labelPos=center' + f' --prop labelFont=12:FFFFFF:true') + +# -------------------------------------------------------------------------- +# Chart 4: Pie with gradient fills, leader lines, and legend positioning +# +# officecli add charts-pie.xlsx "/1-Pie Charts" --type chart \ +# --prop chartType=pie \ +# --prop title="Q4 Distribution" \ +# --prop series1="Q4:198,158,142,180" \ +# --prop categories=East,South,North,West \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop 'gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90;70AD47-C5E0B4:90;FFC000-FFF2CC:90' \ +# --prop legend=right --prop legendfont=10:333333:Helvetica \ +# --prop dataLabels=true \ +# --prop dataLabels.showLeaderLines=true \ +# --prop chartFill=FAFAFA --prop roundedCorners=true +# +# Features: gradients (per-slice), legend=right, legendfont, +# dataLabels.showLeaderLines, chartFill, roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Pie Charts" --type chart' + f' --prop chartType=pie' + f' --prop title="Q4 Distribution"' + f' --prop series1=Q4:198,158,142,180' + f' --prop categories=East,South,North,West' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop "gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90;70AD47-C5E0B4:90;FFC000-FFF2CC:90"' + f' --prop legend=right --prop legendfont=10:333333:Helvetica' + f' --prop dataLabels=true' + f' --prop dataLabels.showLeaderLines=true' + f' --prop chartFill=FAFAFA --prop roundedCorners=true') + +# ========================================================================== +# Sheet: 2-Doughnut Charts +# ========================================================================== +print("\n--- 2-Doughnut Charts ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Doughnut Charts"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic doughnut chart +# +# officecli add charts-pie.xlsx "/2-Doughnut Charts" --type chart \ +# --prop chartType=doughnut \ +# --prop title="Channel Mix" \ +# --prop series1="Channel:55,45" \ +# --prop categories=Online,Retail \ +# --prop colors=4472C4,ED7D31 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=center \ +# --prop labelFont=14:FFFFFF:true +# +# Features: chartType=doughnut, center labels +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Doughnut Charts" --type chart' + f' --prop chartType=doughnut' + f' --prop title="Channel Mix"' + f' --prop series1=Channel:55,45' + f' --prop categories=Online,Retail' + f' --prop colors=4472C4,ED7D31' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelPos=center' + f' --prop labelFont=14:FFFFFF:true') + +# -------------------------------------------------------------------------- +# Chart 2: Multi-ring doughnut (multiple series) +# +# officecli add charts-pie.xlsx "/2-Doughnut Charts" --type chart \ +# --prop chartType=doughnut \ +# --prop title="Year-over-Year Comparison" \ +# --prop series1="2024:40,35,25" \ +# --prop series2="2025:45,30,25" \ +# --prop categories=Electronics,Clothing,Food \ +# --prop colors=4472C4,70AD47,FFC000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop series.outline=FFFFFF-1 \ +# --prop legend=bottom +# +# Features: multi-ring doughnut (multiple series = concentric rings), +# series.outline (white separator between slices) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Doughnut Charts" --type chart' + f' --prop chartType=doughnut' + f' --prop title="Year-over-Year Comparison"' + f' --prop series1=2024:40,35,25' + f' --prop series2=2025:45,30,25' + f' --prop categories=Electronics,Clothing,Food' + f' --prop colors=4472C4,70AD47,FFC000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop series.outline=FFFFFF-1' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Styled doughnut with shadow and custom data labels +# +# officecli add charts-pie.xlsx "/2-Doughnut Charts" --type chart \ +# --prop chartType=doughnut \ +# --prop title="Priority Breakdown" \ +# --prop series1="Priority:50,30,20" \ +# --prop categories=High,Medium,Low \ +# --prop colors=C00000,FFC000,70AD47 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop series.shadow=000000-4-315-2-30 \ +# --prop dataLabels=true --prop labelPos=outsideEnd \ +# --prop dataLabels.numFmt=0"%" \ +# --prop title.shadow=000000-3-315-2-30 \ +# --prop plotFill=F5F5F5 +# +# Features: series.shadow on doughnut, title.shadow, plotFill +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Doughnut Charts" --type chart' + f' --prop chartType=doughnut' + f' --prop title="Priority Breakdown"' + f' --prop series1=Priority:50,30,20' + f' --prop categories=High,Medium,Low' + f' --prop colors=C00000,FFC000,70AD47' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop series.shadow=000000-4-315-2-30' + f' --prop dataLabels=true --prop labelPos=outsideEnd' + f' --prop dataLabels.numFmt=0"%"' + f' --prop title.shadow=000000-3-315-2-30' + f' --prop plotFill=F5F5F5') + +# -------------------------------------------------------------------------- +# Chart 4: Doughnut with per-slice gradient and explosion +# +# officecli add charts-pie.xlsx "/2-Doughnut Charts" --type chart \ +# --prop chartType=doughnut \ +# --prop title="Product Revenue" \ +# --prop series1="Revenue:35,25,20,12,8" \ +# --prop categories=Laptop,Phone,Tablet,Jacket,Coffee \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop explosion=8 \ +# --prop 'gradients=1F4E79-5B9BD5:90;C55A11-F4B183:90;548235-A9D18E:90;7F6000-FFD966:90;843C0B-DDA15E:90' \ +# --prop legend=right \ +# --prop dataLabels=true --prop labelPos=bestFit +# +# Features: explosion on doughnut, 5-slice gradients +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Doughnut Charts" --type chart' + f' --prop chartType=doughnut' + f' --prop title="Product Revenue"' + f' --prop series1=Revenue:35,25,20,12,8' + f' --prop categories=Laptop,Phone,Tablet,Jacket,Coffee' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop explosion=8' + f' --prop "gradients=1F4E79-5B9BD5:90;C55A11-F4B183:90;548235-A9D18E:90;7F6000-FFD966:90;843C0B-DDA15E:90"' + f' --prop legend=right' + f' --prop dataLabels=true --prop labelPos=bestFit') + +# Remove blank default Sheet1 (all data is inline) +cli(f'remove "{FILE}" /Sheet1') + +print(f"\nDone! Generated: {FILE}") +print(" 3 sheets (2 chart sheets, 8 charts total)") diff --git a/examples/excel/charts/charts-pie.xlsx b/examples/excel/charts/charts-pie.xlsx new file mode 100644 index 000000000..91fd7af8d Binary files /dev/null and b/examples/excel/charts/charts-pie.xlsx differ diff --git a/examples/excel/charts/charts-radar.md b/examples/excel/charts/charts-radar.md new file mode 100644 index 000000000..f4ae5fdfa --- /dev/null +++ b/examples/excel/charts/charts-radar.md @@ -0,0 +1,174 @@ +# Radar Charts Showcase + +This demo consists of three files that work together: + +- **charts-radar.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-radar.xlsx** — The generated workbook with 5 sheets (1 default + 4 chart sheets, 16 charts total). +- **charts-radar.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-radar.py +# → charts-radar.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Radar Fundamentals + +Four radar chart variants covering standard, marker, and filled styles. + +```bash +# Basic radar (standard) with 3 series +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=standard \ + --prop series1="Alice:85,70,90,60,75" \ + --prop series2="Bob:65,90,70,80,85" \ + --prop categories=Speed,Strength,Stamina,Agility,Accuracy \ + --prop colors=4472C4,ED7D31,70AD47 + +# Radar with markers and data labels +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=marker \ + --prop marker=circle:6:2E75B6 \ + --prop dataLabels=true + +# Filled radar with transparency +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=filled \ + --prop transparency=40 + +# Filled radar with white outline separators +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=filled \ + --prop series.outline=FFFFFF-0.5 \ + --prop transparency=35 +``` + +**Features:** `radar`, `radarStyle=standard/marker/filled`, `marker=circle:6:color`, `transparency`, `series.outline`, `dataLabels`, `legend=bottom` + +### Sheet: 2-Radar Styling + +Four charts demonstrating title styling, shadows, axis fonts, gridlines, and chart area decoration. + +```bash +# Title styling with font, size, color, bold, shadow +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop title.font=Georgia --prop title.size=18 \ + --prop title.color=1F4E79 --prop title.bold=true \ + --prop title.shadow=000000-3-315-2-30 + +# Series shadow on filled radar +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=filled \ + --prop series.shadow=000000-4-315-2-30 \ + --prop transparency=30 + +# Axis font and gridlines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop axisfont=10:333333:Calibri \ + --prop gridlines=D9D9D9:0.5 + +# Chart area styling with fills, corners, borders +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop plotFill=F5F5F5 --prop chartFill=FAFAFA \ + --prop roundedCorners=true \ + --prop chartArea.border=BFBFBF:0.5 \ + --prop plotArea.border=D9D9D9:0.25 +``` + +**Features:** `title.font/size/color/bold/shadow`, `series.shadow`, `axisfont`, `gridlines`, `plotFill`, `chartFill`, `roundedCorners`, `chartArea.border`, `plotArea.border` + +### Sheet: 3-Labels & Legend + +Four charts covering data labels, legend positioning, manual layout, and multi-series comparison. + +```bash +# Data labels with font styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=marker \ + --prop dataLabels=true --prop labelPos=outsideEnd \ + --prop labelFont=9:333333:true \ + --prop marker=circle:6:2E75B6 + +# Legend positioning with overlay +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop legend=right \ + --prop legendfont=10:1F4E79:Calibri \ + --prop legend.overlay=true + +# Manual plot area layout (fractional) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop plotArea.x=0.1 --prop plotArea.y=0.15 \ + --prop plotArea.w=0.8 --prop plotArea.h=0.75 + +# Five series comparison +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop series1="Dev:90,70,80,65,75" \ + --prop series2="QA:60,85,70,80,90" \ + --prop series3="Design:75,80,85,70,60" \ + --prop series4="PM:80,65,75,90,70" \ + --prop series5="DevOps:70,75,60,85,80" \ + --prop colors=4472C4,ED7D31,70AD47,FFC000,7030A0 +``` + +**Features:** `dataLabels`, `labelPos=outsideEnd`, `labelFont`, `legend=right`, `legendfont`, `legend.overlay`, `plotArea.x/y/w/h`, 5+ series on single radar + +### Sheet: 4-Advanced + +Four charts with advanced effects: title glow, many-spoke layouts, themed styling, and overlap visualization. + +```bash +# Title with glow and shadow effects +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop title.glow=4472C4-8 \ + --prop title.shadow=000000-3-315-2-30 \ + --prop marker=diamond:7:2E75B6 + +# 8-spoke radar with benchmark overlay +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=filled \ + --prop categories=Technical,Communication,Leadership,Creativity,Analytical,Teamwork,Adaptability,Initiative \ + --prop gridlines=D9D9D9:0.25 --prop transparency=35 + +# Single-series with themed purple styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=marker \ + --prop colors=7030A0 --prop marker=square:7:7030A0 \ + --prop title.color=7030A0 --prop plotFill=F8F0FF \ + --prop chartArea.border=7030A0:0.5 --prop roundedCorners=true + +# Before/After comparison with low transparency overlap +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=radar \ + --prop radarStyle=filled \ + --prop transparency=20 \ + --prop series.outline=FFFFFF-0.75 \ + --prop chartFill=FAFAFA --prop plotFill=F5F5F5 +``` + +**Features:** `title.glow`, `title.shadow`, `marker=diamond/square`, 8-category spokes, themed color scheme, low-transparency overlap visualization, before/after comparison pattern + +## Inspect the Generated File + +```bash +officecli query charts-radar.xlsx chart +officecli get charts-radar.xlsx "/1-Radar Fundamentals/chart[1]" +``` diff --git a/examples/excel/charts/charts-radar.py b/examples/excel/charts/charts-radar.py new file mode 100644 index 000000000..d90a03e69 --- /dev/null +++ b/examples/excel/charts/charts-radar.py @@ -0,0 +1,575 @@ +#!/usr/bin/env python3 +""" +Radar Charts Showcase — radar with standard, filled, and marker styles. + +Generates: charts-radar.xlsx + +Usage: + python3 charts-radar.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-radar.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Sheet: 1-Radar Fundamentals +# ========================================================================== +print("\n--- 1-Radar Fundamentals ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Radar Fundamentals"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic radar (standard style) with 3 series +# +# officecli add charts-radar.xlsx "/1-Radar Fundamentals" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=standard \ +# --prop title="Athlete Comparison" \ +# --prop series1="Alice:85,70,90,60,75" \ +# --prop series2="Bob:65,90,70,80,85" \ +# --prop series3="Carol:75,80,80,70,65" \ +# --prop categories=Speed,Strength,Stamina,Agility,Accuracy \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop legend=bottom +# +# Features: chartType=radar, radarStyle=standard, 3 series, categories as spokes +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Radar Fundamentals" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=standard' + f' --prop title="Athlete Comparison"' + f' --prop series1=Alice:85,70,90,60,75' + f' --prop series2=Bob:65,90,70,80,85' + f' --prop series3=Carol:75,80,80,70,65' + f' --prop categories=Speed,Strength,Stamina,Agility,Accuracy' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: Radar with markers (marker style) +# +# officecli add charts-radar.xlsx "/1-Radar Fundamentals" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=marker \ +# --prop title="Product Ratings" \ +# --prop series1="Product A:9,7,8,6,8" \ +# --prop series2="Product B:6,9,7,8,5" \ +# --prop categories=Quality,Price,Design,Support,Delivery \ +# --prop colors=2E75B6,C00000 \ +# --prop marker=circle:6:2E75B6 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop legend=bottom \ +# --prop dataLabels=true +# +# Features: radarStyle=marker, marker=circle:6:color, dataLabels +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Radar Fundamentals" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=marker' + f' --prop title="Product Ratings"' + f' --prop series1="Product A:9,7,8,6,8"' + f' --prop series2="Product B:6,9,7,8,5"' + f' --prop categories=Quality,Price,Design,Support,Delivery' + f' --prop colors=2E75B6,C00000' + f' --prop marker=circle:6:2E75B6' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop legend=bottom' + f' --prop dataLabels=true') + +# -------------------------------------------------------------------------- +# Chart 3: Filled radar with transparency +# +# officecli add charts-radar.xlsx "/1-Radar Fundamentals" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=filled \ +# --prop title="Skills Assessment" \ +# --prop series1="Junior:50,40,60,70,55" \ +# --prop series2="Senior:85,80,75,90,80" \ +# --prop categories=Coding,Design,Testing,Communication,Leadership \ +# --prop colors=4472C4,70AD47 \ +# --prop transparency=40 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=bottom +# +# Features: radarStyle=filled, transparency=40 (semi-transparent fill) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Radar Fundamentals" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=filled' + f' --prop title="Skills Assessment"' + f' --prop series1=Junior:50,40,60,70,55' + f' --prop series2=Senior:85,80,75,90,80' + f' --prop categories=Coding,Design,Testing,Communication,Leadership' + f' --prop colors=4472C4,70AD47' + f' --prop transparency=40' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Filled radar with per-series colors and white outline +# +# officecli add charts-radar.xlsx "/1-Radar Fundamentals" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=filled \ +# --prop title="Department Scores" \ +# --prop series1="Engineering:90,75,60,85,70" \ +# --prop series2="Marketing:60,85,80,70,90" \ +# --prop series3="Sales:70,80,75,65,85" \ +# --prop categories=Innovation,Teamwork,Efficiency,Quality,Growth \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop series.outline=FFFFFF-0.5 \ +# --prop transparency=35 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=bottom +# +# Features: filled radar, series.outline (white border between areas), +# 3 overlapping series with transparency +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Radar Fundamentals" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=filled' + f' --prop title="Department Scores"' + f' --prop series1=Engineering:90,75,60,85,70' + f' --prop series2=Marketing:60,85,80,70,90' + f' --prop series3=Sales:70,80,75,65,85' + f' --prop categories=Innovation,Teamwork,Efficiency,Quality,Growth' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop series.outline=FFFFFF-0.5' + f' --prop transparency=35' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 2-Radar Styling +# ========================================================================== +print("\n--- 2-Radar Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Radar Styling"') + +# -------------------------------------------------------------------------- +# Chart 1: Title styling (font, size, color, bold, shadow) +# +# officecli add charts-radar.xlsx "/2-Radar Styling" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=marker \ +# --prop title="Styled Title Demo" \ +# --prop series1="Team A:80,65,90,70,85" \ +# --prop categories=Attack,Defense,Speed,Skill,Stamina \ +# --prop colors=2E75B6 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop title.font=Georgia --prop title.size=18 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop title.shadow=000000-3-315-2-30 +# +# Features: title.font, title.size, title.color, title.bold, title.shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Radar Styling" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=marker' + f' --prop title="Styled Title Demo"' + f' --prop series1="Team A:80,65,90,70,85"' + f' --prop categories=Attack,Defense,Speed,Skill,Stamina' + f' --prop colors=2E75B6' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop title.font=Georgia --prop title.size=18' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop title.shadow=000000-3-315-2-30') + +# -------------------------------------------------------------------------- +# Chart 2: Series shadow effects +# +# officecli add charts-radar.xlsx "/2-Radar Styling" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=filled \ +# --prop title="Shadow Effects" \ +# --prop series1="Region A:75,80,65,90,70" \ +# --prop series2="Region B:60,70,85,75,80" \ +# --prop categories=Revenue,Profit,Growth,Retention,Satisfaction \ +# --prop colors=4472C4,ED7D31 \ +# --prop series.shadow=000000-4-315-2-30 \ +# --prop transparency=30 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop legend=bottom +# +# Features: series.shadow on filled radar, transparency with shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Radar Styling" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=filled' + f' --prop title="Shadow Effects"' + f' --prop series1="Region A:75,80,65,90,70"' + f' --prop series2="Region B:60,70,85,75,80"' + f' --prop categories=Revenue,Profit,Growth,Retention,Satisfaction' + f' --prop colors=4472C4,ED7D31' + f' --prop series.shadow=000000-4-315-2-30' + f' --prop transparency=30' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Axis font and gridlines styling +# +# officecli add charts-radar.xlsx "/2-Radar Styling" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=marker \ +# --prop title="Axis & Gridlines" \ +# --prop series1="Actual:70,85,60,75,80" \ +# --prop series2="Target:80,80,80,80,80" \ +# --prop categories=KPI 1,KPI 2,KPI 3,KPI 4,KPI 5 \ +# --prop colors=4472C4,C00000 \ +# --prop axisfont=10:333333:Calibri \ +# --prop gridlines=D9D9D9:0.5 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=bottom +# +# Features: axisfont (size:color:fontFamily), gridlines (color-width) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Radar Styling" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=marker' + f' --prop title="Axis & Gridlines"' + f' --prop series1=Actual:70,85,60,75,80' + f' --prop series2=Target:80,80,80,80,80' + f' --prop categories=KPI 1,KPI 2,KPI 3,KPI 4,KPI 5' + f' --prop colors=4472C4,C00000' + f' --prop axisfont=10:333333:Calibri' + f' --prop gridlines=D9D9D9:0.5' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Plot fill, chart fill, rounded corners, borders +# +# officecli add charts-radar.xlsx "/2-Radar Styling" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=filled \ +# --prop title="Chart Area Styling" \ +# --prop series1="Score:85,70,90,60,75" \ +# --prop categories=Speed,Power,Technique,Endurance,Flexibility \ +# --prop colors=4472C4 \ +# --prop transparency=25 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop plotFill=F5F5F5 --prop chartFill=FAFAFA \ +# --prop roundedCorners=true \ +# --prop chartArea.border=BFBFBF:0.5 \ +# --prop plotArea.border=D9D9D9:0.25 +# +# Features: plotFill, chartFill, roundedCorners, chartArea.border, +# plotArea.border +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Radar Styling" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=filled' + f' --prop title="Chart Area Styling"' + f' --prop series1=Score:85,70,90,60,75' + f' --prop categories=Speed,Power,Technique,Endurance,Flexibility' + f' --prop colors=4472C4' + f' --prop transparency=25' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop plotFill=F5F5F5 --prop chartFill=FAFAFA' + f' --prop roundedCorners=true' + f' --prop chartArea.border=BFBFBF:0.5' + f' --prop plotArea.border=D9D9D9:0.25') + +# ========================================================================== +# Sheet: 3-Labels & Legend +# ========================================================================== +print("\n--- 3-Labels & Legend ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Labels & Legend"') + +# -------------------------------------------------------------------------- +# Chart 1: Data labels with font styling and position +# +# officecli add charts-radar.xlsx "/3-Labels & Legend" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=marker \ +# --prop title="Data Labels" \ +# --prop series1="Performance:88,72,95,67,81" \ +# --prop categories=Speed,Strength,Stamina,Agility,Accuracy \ +# --prop colors=2E75B6 \ +# --prop marker=circle:6:2E75B6 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=outsideEnd \ +# --prop labelFont=9:333333:true +# +# Features: dataLabels=true, labelPos=outsideEnd, labelFont (size:color:bold) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Labels & Legend" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=marker' + f' --prop title="Data Labels"' + f' --prop series1=Performance:88,72,95,67,81' + f' --prop categories=Speed,Strength,Stamina,Agility,Accuracy' + f' --prop colors=2E75B6' + f' --prop marker=circle:6:2E75B6' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelPos=outsideEnd' + f' --prop labelFont=9:333333:true') + +# -------------------------------------------------------------------------- +# Chart 2: Legend positioning and styling with overlay +# +# officecli add charts-radar.xlsx "/3-Labels & Legend" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=standard \ +# --prop title="Legend Styles" \ +# --prop series1="Alpha:80,60,75,90,70" \ +# --prop series2="Beta:70,80,85,65,75" \ +# --prop series3="Gamma:65,75,70,80,85" \ +# --prop categories=Metric A,Metric B,Metric C,Metric D,Metric E \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop legend=right \ +# --prop legendfont=10:1F4E79:Calibri \ +# --prop legend.overlay=true +# +# Features: legend=right, legendfont (size:color:fontFamily), legend.overlay +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Labels & Legend" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=standard' + f' --prop title="Legend Styles"' + f' --prop series1=Alpha:80,60,75,90,70' + f' --prop series2=Beta:70,80,85,65,75' + f' --prop series3=Gamma:65,75,70,80,85' + f' --prop categories=Metric A,Metric B,Metric C,Metric D,Metric E' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop legend=right' + f' --prop legendfont=10:1F4E79:Calibri' + f' --prop legend.overlay=true') + +# -------------------------------------------------------------------------- +# Chart 3: Manual plot area layout +# +# officecli add charts-radar.xlsx "/3-Labels & Legend" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=filled \ +# --prop title="Custom Layout" \ +# --prop series1="Team:85,70,90,65,80" \ +# --prop categories=Vision,Execution,Culture,Agility,Impact \ +# --prop colors=4472C4 \ +# --prop transparency=30 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop plotArea.x=0.1 --prop plotArea.y=0.15 \ +# --prop plotArea.w=0.8 --prop plotArea.h=0.75 +# +# Features: plotArea.x/y/w/h (fractional manual layout positioning) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Labels & Legend" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=filled' + f' --prop title="Custom Layout"' + f' --prop series1=Team:85,70,90,65,80' + f' --prop categories=Vision,Execution,Culture,Agility,Impact' + f' --prop colors=4472C4' + f' --prop transparency=30' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop plotArea.x=0.1 --prop plotArea.y=0.15' + f' --prop plotArea.w=0.8 --prop plotArea.h=0.75') + +# -------------------------------------------------------------------------- +# Chart 4: Multiple series (5+) comparison +# +# officecli add charts-radar.xlsx "/3-Labels & Legend" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=standard \ +# --prop title="Multi-Team Comparison" \ +# --prop series1="Dev:90,70,80,65,75" \ +# --prop series2="QA:60,85,70,80,90" \ +# --prop series3="Design:75,80,85,70,60" \ +# --prop series4="PM:80,65,75,90,70" \ +# --prop series5="DevOps:70,75,60,85,80" \ +# --prop categories=Speed,Quality,Innovation,Teamwork,Delivery \ +# --prop colors=4472C4,ED7D31,70AD47,FFC000,7030A0 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=bottom \ +# --prop legendfont=9:333333:Calibri +# +# Features: 5 series on one radar, distinguishing many overlapping lines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Labels & Legend" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=standard' + f' --prop title="Multi-Team Comparison"' + f' --prop series1=Dev:90,70,80,65,75' + f' --prop series2=QA:60,85,70,80,90' + f' --prop series3=Design:75,80,85,70,60' + f' --prop series4=PM:80,65,75,90,70' + f' --prop series5=DevOps:70,75,60,85,80' + f' --prop categories=Speed,Quality,Innovation,Teamwork,Delivery' + f' --prop colors=4472C4,ED7D31,70AD47,FFC000,7030A0' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=bottom' + f' --prop legendfont=9:333333:Calibri') + +# ========================================================================== +# Sheet: 4-Advanced +# ========================================================================== +print("\n--- 4-Advanced ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Advanced"') + +# -------------------------------------------------------------------------- +# Chart 1: Title glow and shadow effects +# +# officecli add charts-radar.xlsx "/4-Advanced" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=marker \ +# --prop title="Glow & Shadow Title" \ +# --prop series1="Score:75,85,65,90,80" \ +# --prop categories=Creativity,Logic,Memory,Focus,Speed \ +# --prop colors=2E75B6 \ +# --prop marker=diamond:7:2E75B6 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop title.font=Georgia --prop title.size=16 \ +# --prop title.bold=true --prop title.color=1F4E79 \ +# --prop title.glow=4472C4-8 \ +# --prop title.shadow=000000-3-315-2-30 +# +# Features: title.glow (color-radius), title.shadow combined +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Advanced" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=marker' + f' --prop title="Glow & Shadow Title"' + f' --prop series1=Score:75,85,65,90,80' + f' --prop categories=Creativity,Logic,Memory,Focus,Speed' + f' --prop colors=2E75B6' + f' --prop marker=diamond:7:2E75B6' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop title.font=Georgia --prop title.size=16' + f' --prop title.bold=true --prop title.color=1F4E79' + f' --prop title.glow=4472C4-8' + f' --prop title.shadow=000000-3-315-2-30') + +# -------------------------------------------------------------------------- +# Chart 2: Radar with many spokes (8 categories) +# +# officecli add charts-radar.xlsx "/4-Advanced" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=filled \ +# --prop title="8-Spoke Assessment" \ +# --prop series1="Candidate:85,70,90,60,75,80,65,88" \ +# --prop series2="Benchmark:70,70,70,70,70,70,70,70" \ +# --prop categories=Technical,Communication,Leadership,Creativity,Analytical,Teamwork,Adaptability,Initiative \ +# --prop colors=4472C4,BFBFBF \ +# --prop transparency=35 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop legend=bottom \ +# --prop gridlines=D9D9D9:0.25 +# +# Features: 8 categories (many spokes), benchmark overlay, gridlines +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Advanced" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=filled' + f' --prop title="8-Spoke Assessment"' + f' --prop series1=Candidate:85,70,90,60,75,80,65,88' + f' --prop series2=Benchmark:70,70,70,70,70,70,70,70' + f' --prop categories=Technical,Communication,Leadership,Creativity,Analytical,Teamwork,Adaptability,Initiative' + f' --prop colors=4472C4,BFBFBF' + f' --prop transparency=35' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop legend=bottom' + f' --prop gridlines=D9D9D9:0.25') + +# -------------------------------------------------------------------------- +# Chart 3: Single-series radar with full styling +# +# officecli add charts-radar.xlsx "/4-Advanced" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=marker \ +# --prop title="Personal Profile" \ +# --prop series1="Self:92,78,85,65,88,70" \ +# --prop categories=Python,JavaScript,SQL,DevOps,Testing,Design \ +# --prop colors=7030A0 \ +# --prop marker=square:7:7030A0 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelFont=9:7030A0:true \ +# --prop title.font=Calibri --prop title.size=14 \ +# --prop title.color=7030A0 --prop title.bold=true \ +# --prop plotFill=F8F0FF --prop chartFill=FFFFFF \ +# --prop roundedCorners=true \ +# --prop chartArea.border=7030A0:0.5 +# +# Features: single series with marker, full title/chart/plot styling, +# themed color scheme (purple) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Advanced" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=marker' + f' --prop title="Personal Profile"' + f' --prop series1=Self:92,78,85,65,88,70' + f' --prop categories=Python,JavaScript,SQL,DevOps,Testing,Design' + f' --prop colors=7030A0' + f' --prop marker=square:7:7030A0' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelFont=9:7030A0:true' + f' --prop title.font=Calibri --prop title.size=14' + f' --prop title.color=7030A0 --prop title.bold=true' + f' --prop plotFill=F8F0FF --prop chartFill=FFFFFF' + f' --prop roundedCorners=true' + f' --prop chartArea.border=7030A0:0.5') + +# -------------------------------------------------------------------------- +# Chart 4: Two-series filled radar with low transparency for overlap +# +# officecli add charts-radar.xlsx "/4-Advanced" --type chart \ +# --prop chartType=radar \ +# --prop radarStyle=filled \ +# --prop title="Before vs After" \ +# --prop series1="Before:55,40,65,50,45" \ +# --prop series2="After:85,75,80,70,80" \ +# --prop categories=Revenue,Efficiency,Satisfaction,Innovation,Retention \ +# --prop colors=C00000,70AD47 \ +# --prop transparency=20 \ +# --prop series.outline=FFFFFF-0.75 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=bottom \ +# --prop dataLabels=true --prop labelFont=9:333333:false \ +# --prop chartFill=FAFAFA --prop plotFill=F5F5F5 +# +# Features: low transparency (20%) for visible overlap, before/after +# comparison pattern, series.outline for separation +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Advanced" --type chart' + f' --prop chartType=radar' + f' --prop radarStyle=filled' + f' --prop title="Before vs After"' + f' --prop series1=Before:55,40,65,50,45' + f' --prop series2=After:85,75,80,70,80' + f' --prop categories=Revenue,Efficiency,Satisfaction,Innovation,Retention' + f' --prop colors=C00000,70AD47' + f' --prop transparency=20' + f' --prop series.outline=FFFFFF-0.75' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=bottom' + f' --prop dataLabels=true --prop labelFont=9:333333:false' + f' --prop chartFill=FAFAFA --prop plotFill=F5F5F5') + +# Remove blank default Sheet1 (all data is inline) +cli(f'remove "{FILE}" /Sheet1') + +print(f"\nDone! Generated: {FILE}") +print(" 5 sheets (4 chart sheets, 16 charts total)") diff --git a/examples/excel/charts/charts-radar.xlsx b/examples/excel/charts/charts-radar.xlsx new file mode 100644 index 000000000..3d70eec1e Binary files /dev/null and b/examples/excel/charts/charts-radar.xlsx differ diff --git a/examples/excel/charts/charts-scatter.md b/examples/excel/charts/charts-scatter.md new file mode 100644 index 000000000..b89ab3b9c --- /dev/null +++ b/examples/excel/charts/charts-scatter.md @@ -0,0 +1,217 @@ +# Scatter Charts Showcase + +This demo consists of three files that work together: + +- **charts-scatter.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-scatter.xlsx** — The generated workbook with 7 sheets (1 default + 6 chart sheets, 24 charts total). +- **charts-scatter.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-scatter.py +# → charts-scatter.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Scatter Fundamentals + +Four scatter variants covering markers+lines, marker-only, smooth curves, and line-only. + +```bash +# Basic scatter with circle markers and connecting lines +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop series1="Male:62,68,72,78,82,88,95" \ + --prop categories=160,165,170,175,180,185,190 \ + --prop marker=circle --prop markerSize=6 --prop lineWidth=1.5 \ + --prop catTitle=Height (cm) --prop axisTitle=Weight (kg) + +# Scatter marker-only (no connecting lines) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter --prop scatterStyle=marker \ + --prop markerSize=8 --prop gridlines=D9D9D9:0.5:dot + +# Scatter smooth curve (Bezier interpolation) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter --prop scatterStyle=smooth \ + --prop smooth=true --prop marker=diamond --prop lineWidth=2 + +# Scatter line-only (no markers) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter --prop scatterStyle=line \ + --prop showMarker=false --prop lineWidth=2.5 --prop lineDash=dash +``` + +**Features:** `scatter`, `scatterStyle=marker|smooth|line`, `smooth=true`, `marker=circle|diamond`, `markerSize`, `lineWidth`, `lineDash=dash`, `showMarker=false`, `catTitle`, `axisTitle`, `gridlines` + +### Sheet: 2-Marker Styles + +Four charts demonstrating all marker shapes and per-series marker control. + +```bash +# Per-series markers: circle, diamond, square +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop series1.marker=circle --prop series2.marker=diamond \ + --prop series3.marker=square --prop markerSize=8 + +# Per-series markers: triangle, star, x +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop series1.marker=triangle --prop series2.marker=star \ + --prop series3.marker=x --prop markerSize=9 + +# Large markers with plus and dash shapes +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter --prop scatterStyle=marker \ + --prop series1.marker=circle --prop series2.marker=plus \ + --prop series3.marker=dash --prop markerSize=10 + +# showMarker=false with lineDash=dashDot +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter --prop scatterStyle=lineMarker \ + --prop showMarker=false --prop lineDash=dashDot +``` + +**Features:** `series{N}.marker=circle|diamond|square|triangle|star|x|plus|dash`, `markerSize`, `scatterStyle=lineMarker|marker`, `showMarker=false`, `lineDash=dashDot` + +### Sheet: 3-Trendlines + +Four charts covering all trendline types and sub-properties. + +```bash +# Linear trendline with equation display +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter --prop scatterStyle=marker \ + --prop trendline=linear \ + --prop series1.trendline.equation=true + +# Polynomial (order 3) with R-squared display +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter --prop scatterStyle=marker \ + --prop trendline=poly:3 \ + --prop series1.trendline.rsquared=true + +# Exponential with forward/backward extrapolation +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter --prop scatterStyle=marker \ + --prop trendline=exp:2:1 \ + --prop series1.trendline.name=Exponential Fit + +# Per-series trendlines: linear vs logarithmic +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter --prop scatterStyle=marker \ + --prop series1.trendline=linear --prop series2.trendline=log \ + --prop series1.trendline.equation=true \ + --prop series2.trendline.rsquared=true +``` + +**Features:** `trendline=linear|poly:N|exp|log|power|movingAvg`, `trendline=exp:forward:backward` (extrapolation), `series{N}.trendline` (per-series), `series{N}.trendline.equation`, `series{N}.trendline.rsquared`, `series{N}.trendline.name` + +### Sheet: 4-Error Bars + +Four charts covering all error bar types on scatter series. + +```bash +# Fixed error bars (+/-5) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop errBars=fixed:5 + +# Percentage error bars (10%) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop errBars=percent:10 + +# Standard deviation error bars +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop errBars=stddev + +# Standard error with series shadow +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop errBars=stderr \ + --prop series.shadow=000000-4-315-2-30 +``` + +**Features:** `errBars=fixed:N|percent:N|stddev|stderr`, `series.shadow` + +### Sheet: 5-Styling + +Four charts covering title styling, fills, gradients, borders, and axis formatting. + +```bash +# Title styling with series shadow and outline +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop title.font=Georgia --prop title.size=16 \ + --prop title.color=1F4E79 --prop title.bold=true \ + --prop title.shadow=000000-3-315-2-30 \ + --prop series.shadow=000000-4-315-2-30 \ + --prop series.outline=333333-1.5 + +# Gradients, transparency, plotFill, chartFill +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop 'gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90' \ + --prop transparency=20 \ + --prop plotFill=F5F5F5 --prop chartFill=FAFAFA + +# Axis font, gridlines, minor gridlines, axis line +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop axisfont=9:C00000:Arial \ + --prop gridlines=BFBFBF:0.75:solid \ + --prop minorGridlines=E0E0E0:0.25:dot \ + --prop axisLine=333333:1 + +# Chart/plot borders and rounded corners +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop chartArea.border=333333-1.5 \ + --prop plotArea.border=999999-0.75 \ + --prop roundedCorners=true +``` + +**Features:** `title.font/size/color/bold`, `title.shadow`, `series.shadow`, `series.outline`, `gradients`, `transparency`, `plotFill`, `chartFill`, `axisfont`, `gridlines`, `minorGridlines`, `axisLine`, `chartArea.border`, `plotArea.border`, `roundedCorners` + +### Sheet: 6-Advanced + +Four charts covering secondary axis, reference lines, log scale, and conditional coloring. + +```bash +# Secondary Y-axis for dual-unit scatter +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop secondaryAxis=2 + +# Reference line (horizontal target) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop referenceLine=75:FF0000:Target:dash + +# Logarithmic axis with min/max bounds +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter \ + --prop logBase=10 \ + --prop axisMin=1 --prop axisMax=10000 + +# Data labels with conditional color rule +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=scatter --prop scatterStyle=marker \ + --prop dataLabels=true --prop labelPos=top \ + --prop colorRule=60:C00000:00AA00 +``` + +**Features:** `secondaryAxis`, `referenceLine=value:color:label:dash`, `logBase`, `axisMin`, `axisMax`, `dataLabels`, `labelPos=top`, `colorRule=threshold:belowColor:aboveColor` + +## Inspect the Generated File + +```bash +officecli query charts-scatter.xlsx chart +officecli get charts-scatter.xlsx "/1-Scatter Fundamentals/chart[1]" +``` diff --git a/examples/excel/charts/charts-scatter.py b/examples/excel/charts/charts-scatter.py new file mode 100644 index 000000000..a4206bc46 --- /dev/null +++ b/examples/excel/charts/charts-scatter.py @@ -0,0 +1,873 @@ +#!/usr/bin/env python3 +""" +Scatter Charts Showcase — scatter with all marker, trendline, error bar, and styling variations. + +Generates: charts-scatter.xlsx + +Every scatter chart feature officecli supports is demonstrated at least once: +scatter styles, marker types, smooth curves, trendlines (linear, polynomial, +exponential, logarithmic, power, movingAvg), error bars, axis scaling, +gridlines, data labels, legend, fills, shadows, borders, secondary axis, +reference lines, log scale, and color rules. + +6 sheets, 24 charts total. + + 1-Scatter Fundamentals 4 charts — basic scatter, marker-only, smooth curve, line-only + 2-Marker Styles 4 charts — per-series markers, shapes, sizes, toggle + 3-Trendlines 4 charts — linear, polynomial, exponential, per-series + 4-Error Bars 4 charts — fixed, percent, stddev, stderr + 5-Styling 4 charts — title/shadow, gradients, axis/grid, borders + 6-Advanced 4 charts — secondary axis, reference line, log scale, color rule + +Usage: + python3 charts-scatter.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-scatter.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Sheet: 1-Scatter Fundamentals +# ========================================================================== +print("\n--- 1-Scatter Fundamentals ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Scatter Fundamentals"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic scatter with circle markers and connecting lines +# +# officecli add charts-scatter.xlsx "/1-Scatter Fundamentals" --type chart \ +# --prop chartType=scatter \ +# --prop title="Height vs Weight" \ +# --prop categories=160,165,170,175,180,185,190 \ +# --prop series1="Male:62,68,72,78,82,88,95" \ +# --prop series2="Female:50,55,58,62,65,70,74" \ +# --prop colors=2E75B6,ED7D31 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop marker=circle --prop markerSize=6 \ +# --prop lineWidth=1.5 \ +# --prop catTitle=Height (cm) --prop axisTitle=Weight (kg) \ +# --prop legend=bottom +# +# Features: chartType=scatter, marker=circle, markerSize=6, lineWidth=1.5, +# catTitle, axisTitle, legend +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Scatter Fundamentals" --type chart' + f' --prop chartType=scatter' + f' --prop title="Height vs Weight"' + f' --prop categories=160,165,170,175,180,185,190' + f' --prop series1=Male:62,68,72,78,82,88,95' + f' --prop series2=Female:50,55,58,62,65,70,74' + f' --prop colors=2E75B6,ED7D31' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop marker=circle --prop markerSize=6' + f' --prop lineWidth=1.5' + f' --prop catTitle="Height (cm)" --prop axisTitle="Weight (kg)"' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: Scatter marker-only (scatterStyle=marker), various marker sizes +# +# officecli add charts-scatter.xlsx "/1-Scatter Fundamentals" --type chart \ +# --prop chartType=scatter \ +# --prop scatterStyle=marker \ +# --prop title="Study Hours vs Test Score" \ +# --prop categories=1,2,3,4,5,6,7,8 \ +# --prop series1="Class A:55,60,65,72,78,82,88,92" \ +# --prop series2="Class B:50,58,62,68,74,80,85,90" \ +# --prop colors=4472C4,70AD47 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop markerSize=8 \ +# --prop catTitle=Study Hours --prop axisTitle=Score \ +# --prop gridlines=D9D9D9:0.5:dot +# +# Features: scatterStyle=marker (no connecting lines), markerSize=8, +# gridlines styling +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Scatter Fundamentals" --type chart' + f' --prop chartType=scatter' + f' --prop scatterStyle=marker' + f' --prop title="Study Hours vs Test Score"' + f' --prop categories=1,2,3,4,5,6,7,8' + f' --prop series1="Class A:55,60,65,72,78,82,88,92"' + f' --prop series2="Class B:50,58,62,68,74,80,85,90"' + f' --prop colors=4472C4,70AD47' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop markerSize=8' + f' --prop catTitle="Study Hours" --prop axisTitle=Score' + f' --prop gridlines=D9D9D9:0.5:dot') + +# -------------------------------------------------------------------------- +# Chart 3: Scatter smooth curve (smooth=true, scatterStyle=smooth) +# +# officecli add charts-scatter.xlsx "/1-Scatter Fundamentals" --type chart \ +# --prop chartType=scatter \ +# --prop scatterStyle=smooth \ +# --prop smooth=true \ +# --prop title="Temperature vs Ice Cream Sales" \ +# --prop categories=15,18,22,25,28,30,33,35 \ +# --prop series1="Sales ($):120,180,260,340,420,480,530,560" \ +# --prop colors=C00000 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop marker=diamond --prop markerSize=7 \ +# --prop lineWidth=2 \ +# --prop catTitle=Temperature (C) --prop axisTitle=Daily Sales ($) +# +# Features: scatterStyle=smooth, smooth=true (Bezier interpolation), +# marker=diamond, single series +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Scatter Fundamentals" --type chart' + f' --prop chartType=scatter' + f' --prop scatterStyle=smooth' + f' --prop smooth=true' + f' --prop title="Temperature vs Ice Cream Sales"' + f' --prop categories=15,18,22,25,28,30,33,35' + f' --prop series1="Sales ($):120,180,260,340,420,480,530,560"' + f' --prop colors=C00000' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop marker=diamond --prop markerSize=7' + f' --prop lineWidth=2' + f' --prop catTitle="Temperature (C)" --prop axisTitle="Daily Sales ($)"') + +# -------------------------------------------------------------------------- +# Chart 4: Scatter line-only (no markers, scatterStyle=line) +# +# officecli add charts-scatter.xlsx "/1-Scatter Fundamentals" --type chart \ +# --prop chartType=scatter \ +# --prop scatterStyle=line \ +# --prop title="Altitude vs Air Pressure" \ +# --prop categories=0,500,1000,2000,3000,5000,8000 \ +# --prop series1="Pressure (hPa):1013,955,899,795,701,540,356" \ +# --prop colors=1F4E79 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop showMarker=false \ +# --prop lineWidth=2.5 \ +# --prop lineDash=dash \ +# --prop catTitle=Altitude (m) --prop axisTitle=Pressure (hPa) +# +# Features: scatterStyle=line (line without markers), showMarker=false, +# lineWidth=2.5, lineDash=dash +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Scatter Fundamentals" --type chart' + f' --prop chartType=scatter' + f' --prop scatterStyle=line' + f' --prop title="Altitude vs Air Pressure"' + f' --prop categories=0,500,1000,2000,3000,5000,8000' + f' --prop series1="Pressure (hPa):1013,955,899,795,701,540,356"' + f' --prop colors=1F4E79' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop showMarker=false' + f' --prop lineWidth=2.5' + f' --prop lineDash=dash' + f' --prop catTitle="Altitude (m)" --prop axisTitle="Pressure (hPa)"') + +# ========================================================================== +# Sheet: 2-Marker Styles +# ========================================================================== +print("\n--- 2-Marker Styles ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Marker Styles"') + +# -------------------------------------------------------------------------- +# Chart 1: Per-series markers — circle, diamond, square +# +# officecli add charts-scatter.xlsx "/2-Marker Styles" --type chart \ +# --prop chartType=scatter \ +# --prop title="Per-Series Markers: Circle, Diamond, Square" \ +# --prop categories=10,20,30,40,50,60 \ +# --prop series1="Sensor A:12,28,35,42,55,68" \ +# --prop series2="Sensor B:8,22,30,38,48,58" \ +# --prop series3="Sensor C:15,25,32,45,52,62" \ +# --prop colors=4472C4,ED7D31,70AD47 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop series1.marker=circle \ +# --prop series2.marker=diamond \ +# --prop series3.marker=square \ +# --prop markerSize=8 --prop lineWidth=1 \ +# --prop legend=bottom +# +# Features: series1.marker=circle, series2.marker=diamond, +# series3.marker=square (per-series marker style) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Marker Styles" --type chart' + f' --prop chartType=scatter' + f' --prop title="Per-Series Markers: Circle, Diamond, Square"' + f' --prop categories=10,20,30,40,50,60' + f' --prop series1="Sensor A:12,28,35,42,55,68"' + f' --prop series2="Sensor B:8,22,30,38,48,58"' + f' --prop series3="Sensor C:15,25,32,45,52,62"' + f' --prop colors=4472C4,ED7D31,70AD47' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop series1.marker=circle' + f' --prop series2.marker=diamond' + f' --prop series3.marker=square' + f' --prop markerSize=8 --prop lineWidth=1' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: Per-series markers — triangle, star, x +# +# officecli add charts-scatter.xlsx "/2-Marker Styles" --type chart \ +# --prop chartType=scatter \ +# --prop title="Per-Series Markers: Triangle, Star, X" \ +# --prop categories=5,10,15,20,25,30 \ +# --prop series1="Lab 1:18,32,28,45,52,60" \ +# --prop series2="Lab 2:22,25,38,40,48,55" \ +# --prop series3="Lab 3:10,20,32,35,42,50" \ +# --prop colors=FFC000,9DC3E6,843C0B \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop series1.marker=triangle \ +# --prop series2.marker=star \ +# --prop series3.marker=x \ +# --prop markerSize=9 --prop lineWidth=1 \ +# --prop legend=bottom +# +# Features: series1.marker=triangle, series2.marker=star, +# series3.marker=x +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Marker Styles" --type chart' + f' --prop chartType=scatter' + f' --prop title="Per-Series Markers: Triangle, Star, X"' + f' --prop categories=5,10,15,20,25,30' + f' --prop series1="Lab 1:18,32,28,45,52,60"' + f' --prop series2="Lab 2:22,25,38,40,48,55"' + f' --prop series3="Lab 3:10,20,32,35,42,50"' + f' --prop colors=FFC000,9DC3E6,843C0B' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop series1.marker=triangle' + f' --prop series2.marker=star' + f' --prop series3.marker=x' + f' --prop markerSize=9 --prop lineWidth=1' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Large markers with series colors, markerSize=10 +# +# officecli add charts-scatter.xlsx "/2-Marker Styles" --type chart \ +# --prop chartType=scatter \ +# --prop scatterStyle=marker \ +# --prop title="Large Markers (size=10)" \ +# --prop categories=100,200,300,400,500 \ +# --prop series1="Revenue:150,280,350,420,510" \ +# --prop series2="Profit:80,140,180,220,280" \ +# --prop series3="Cost:70,140,170,200,230" \ +# --prop colors=2E75B6,548235,BF8F00 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop series1.marker=circle \ +# --prop series2.marker=plus \ +# --prop series3.marker=dash \ +# --prop markerSize=10 \ +# --prop legend=right +# +# Features: markerSize=10, marker=plus, marker=dash, scatterStyle=marker +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Marker Styles" --type chart' + f' --prop chartType=scatter' + f' --prop scatterStyle=marker' + f' --prop title="Large Markers (size=10)"' + f' --prop categories=100,200,300,400,500' + f' --prop series1="Revenue:150,280,350,420,510"' + f' --prop series2="Profit:80,140,180,220,280"' + f' --prop series3="Cost:70,140,170,200,230"' + f' --prop colors=2E75B6,548235,BF8F00' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop series1.marker=circle' + f' --prop series2.marker=plus' + f' --prop series3.marker=dash' + f' --prop markerSize=10' + f' --prop legend=right') + +# -------------------------------------------------------------------------- +# Chart 4: showMarker=false (line only) vs showMarker=true +# +# officecli add charts-scatter.xlsx "/2-Marker Styles" --type chart \ +# --prop chartType=scatter \ +# --prop scatterStyle=lineMarker \ +# --prop title="Marker Toggle (none shown)" \ +# --prop categories=1,2,3,4,5,6,7,8,9,10 \ +# --prop series1="Signal:3,7,5,11,9,14,12,18,15,20" \ +# --prop series2="Noise:2,4,6,5,8,7,10,9,12,11" \ +# --prop colors=4472C4,BFBFBF \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop showMarker=false \ +# --prop lineWidth=2 \ +# --prop lineDash=dashDot \ +# --prop legend=bottom +# +# Features: scatterStyle=lineMarker, showMarker=false (markers hidden), +# lineDash=dashDot +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Marker Styles" --type chart' + f' --prop chartType=scatter' + f' --prop scatterStyle=lineMarker' + f' --prop title="Marker Toggle (none shown)"' + f' --prop categories=1,2,3,4,5,6,7,8,9,10' + f' --prop series1="Signal:3,7,5,11,9,14,12,18,15,20"' + f' --prop series2="Noise:2,4,6,5,8,7,10,9,12,11"' + f' --prop colors=4472C4,BFBFBF' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop showMarker=false' + f' --prop lineWidth=2' + f' --prop lineDash=dashDot' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 3-Trendlines +# ========================================================================== +print("\n--- 3-Trendlines ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Trendlines"') + +# -------------------------------------------------------------------------- +# Chart 1: Linear trendline with equation display +# +# officecli add charts-scatter.xlsx "/3-Trendlines" --type chart \ +# --prop chartType=scatter \ +# --prop scatterStyle=marker \ +# --prop title="Linear Trendline + Equation" \ +# --prop categories=1,2,3,4,5,6,7,8,9,10 \ +# --prop series1="Observed:8,15,22,28,33,42,48,55,60,68" \ +# --prop colors=4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop markerSize=7 \ +# --prop trendline=linear \ +# --prop series1.trendline.equation=true \ +# --prop catTitle=X --prop axisTitle=Y +# +# Features: trendline=linear, series1.trendline.equation=true +# (display equation on chart) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Trendlines" --type chart' + f' --prop chartType=scatter' + f' --prop scatterStyle=marker' + f' --prop title="Linear Trendline + Equation"' + f' --prop categories=1,2,3,4,5,6,7,8,9,10' + f' --prop series1="Observed:8,15,22,28,33,42,48,55,60,68"' + f' --prop colors=4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop markerSize=7' + f' --prop trendline=linear' + f' --prop series1.trendline.equation=true' + f' --prop catTitle=X --prop axisTitle=Y') + +# -------------------------------------------------------------------------- +# Chart 2: Polynomial trendline (order 3) with R-squared display +# +# officecli add charts-scatter.xlsx "/3-Trendlines" --type chart \ +# --prop chartType=scatter \ +# --prop scatterStyle=marker \ +# --prop title="Polynomial (order 3) + R-squared" \ +# --prop categories=1,2,3,4,5,6,7,8,9,10 \ +# --prop series1="Measurement:5,12,25,30,28,35,50,62,58,72" \ +# --prop colors=70AD47 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop markerSize=7 --prop marker=square \ +# --prop trendline=poly:3 \ +# --prop series1.trendline.rsquared=true \ +# --prop catTitle=Sample --prop axisTitle=Value +# +# Features: trendline=poly:3 (polynomial order 3), +# series1.trendline.rsquared=true (R-squared display) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Trendlines" --type chart' + f' --prop chartType=scatter' + f' --prop scatterStyle=marker' + f' --prop title="Polynomial (order 3) + R-squared"' + f' --prop categories=1,2,3,4,5,6,7,8,9,10' + f' --prop series1="Measurement:5,12,25,30,28,35,50,62,58,72"' + f' --prop colors=70AD47' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop markerSize=7 --prop marker=square' + f' --prop trendline=poly:3' + f' --prop series1.trendline.rsquared=true' + f' --prop catTitle=Sample --prop axisTitle=Value') + +# -------------------------------------------------------------------------- +# Chart 3: Exponential trendline with forward/backward extrapolation +# +# officecli add charts-scatter.xlsx "/3-Trendlines" --type chart \ +# --prop chartType=scatter \ +# --prop scatterStyle=marker \ +# --prop title="Exponential + Extrapolation" \ +# --prop categories=1,2,3,4,5,6,7,8 \ +# --prop series1="Growth:2,4,7,12,20,35,58,95" \ +# --prop colors=ED7D31 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop markerSize=7 --prop marker=triangle \ +# --prop trendline=exp:2:1 \ +# --prop series1.trendline.name=Exponential Fit \ +# --prop catTitle=Period --prop axisTitle=Amount +# +# Features: trendline=exp:2:1 (exponential, forward=2, backward=1), +# series1.trendline.name (custom trendline label) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Trendlines" --type chart' + f' --prop chartType=scatter' + f' --prop scatterStyle=marker' + f' --prop title="Exponential + Extrapolation"' + f' --prop categories=1,2,3,4,5,6,7,8' + f' --prop series1="Growth:2,4,7,12,20,35,58,95"' + f' --prop colors=ED7D31' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop markerSize=7 --prop marker=triangle' + f' --prop trendline=exp:2:1' + f' --prop series1.trendline.name="Exponential Fit"' + f' --prop catTitle=Period --prop axisTitle=Amount') + +# -------------------------------------------------------------------------- +# Chart 4: Per-series trendlines — linear vs logarithmic +# +# officecli add charts-scatter.xlsx "/3-Trendlines" --type chart \ +# --prop chartType=scatter \ +# --prop scatterStyle=marker \ +# --prop title="Per-Series: Linear vs Logarithmic" \ +# --prop categories=1,2,4,8,16,32,64 \ +# --prop series1="Dataset A:10,18,30,45,62,78,95" \ +# --prop series2="Dataset B:5,25,38,45,50,54,56" \ +# --prop colors=4472C4,C00000 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop markerSize=7 \ +# --prop series1.trendline=linear \ +# --prop series2.trendline=log \ +# --prop series1.trendline.equation=true \ +# --prop series2.trendline.rsquared=true \ +# --prop legend=bottom +# +# Features: series1.trendline=linear, series2.trendline=log, +# per-series trendline with sub-properties +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Trendlines" --type chart' + f' --prop chartType=scatter' + f' --prop scatterStyle=marker' + f' --prop title="Per-Series: Linear vs Logarithmic"' + f' --prop categories=1,2,4,8,16,32,64' + f' --prop series1="Dataset A:10,18,30,45,62,78,95"' + f' --prop series2="Dataset B:5,25,38,45,50,54,56"' + f' --prop colors=4472C4,C00000' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop markerSize=7' + f' --prop series1.trendline=linear' + f' --prop series2.trendline=log' + f' --prop series1.trendline.equation=true' + f' --prop series2.trendline.rsquared=true' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 4-Error Bars +# ========================================================================== +print("\n--- 4-Error Bars ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Error Bars"') + +# -------------------------------------------------------------------------- +# Chart 1: Fixed error bars (errBars=fixed:5) +# +# officecli add charts-scatter.xlsx "/4-Error Bars" --type chart \ +# --prop chartType=scatter \ +# --prop title="Fixed Error Bars (+-5)" \ +# --prop categories=10,20,30,40,50,60 \ +# --prop series1="Measurement:25,42,58,72,88,105" \ +# --prop colors=4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop marker=circle --prop markerSize=7 \ +# --prop lineWidth=1 \ +# --prop errBars=fixed:5 \ +# --prop catTitle=Input --prop axisTitle=Output +# +# Features: errBars=fixed:5 (constant +/-5 error) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Error Bars" --type chart' + f' --prop chartType=scatter' + f' --prop title="Fixed Error Bars (+-5)"' + f' --prop categories=10,20,30,40,50,60' + f' --prop series1="Measurement:25,42,58,72,88,105"' + f' --prop colors=4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop marker=circle --prop markerSize=7' + f' --prop lineWidth=1' + f' --prop errBars=fixed:5' + f' --prop catTitle=Input --prop axisTitle=Output') + +# -------------------------------------------------------------------------- +# Chart 2: Percentage error bars (errBars=percent:10) +# +# officecli add charts-scatter.xlsx "/4-Error Bars" --type chart \ +# --prop chartType=scatter \ +# --prop title="Percentage Error Bars (10%)" \ +# --prop categories=5,10,15,20,25,30 \ +# --prop series1="Yield:120,185,240,310,375,450" \ +# --prop colors=70AD47 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop marker=diamond --prop markerSize=7 \ +# --prop lineWidth=1 \ +# --prop errBars=percent:10 \ +# --prop catTitle=Dosage --prop axisTitle=Yield +# +# Features: errBars=percent:10 (10% of each value) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Error Bars" --type chart' + f' --prop chartType=scatter' + f' --prop title="Percentage Error Bars (10%)"' + f' --prop categories=5,10,15,20,25,30' + f' --prop series1="Yield:120,185,240,310,375,450"' + f' --prop colors=70AD47' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop marker=diamond --prop markerSize=7' + f' --prop lineWidth=1' + f' --prop errBars=percent:10' + f' --prop catTitle=Dosage --prop axisTitle=Yield') + +# -------------------------------------------------------------------------- +# Chart 3: Standard deviation error bars (errBars=stddev) +# +# officecli add charts-scatter.xlsx "/4-Error Bars" --type chart \ +# --prop chartType=scatter \ +# --prop title="Standard Deviation Error Bars" \ +# --prop categories=0,1,2,3,4,5,6,7 \ +# --prop series1="Trial 1:48,52,47,55,50,53,49,51" \ +# --prop series2="Trial 2:30,35,28,40,32,38,34,36" \ +# --prop colors=ED7D31,9DC3E6 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop marker=square --prop markerSize=6 \ +# --prop lineWidth=1 \ +# --prop errBars=stddev \ +# --prop legend=bottom +# +# Features: errBars=stddev (standard deviation), multi-series with errBars +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Error Bars" --type chart' + f' --prop chartType=scatter' + f' --prop title="Standard Deviation Error Bars"' + f' --prop categories=0,1,2,3,4,5,6,7' + f' --prop series1="Trial 1:48,52,47,55,50,53,49,51"' + f' --prop series2="Trial 2:30,35,28,40,32,38,34,36"' + f' --prop colors=ED7D31,9DC3E6' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop marker=square --prop markerSize=6' + f' --prop lineWidth=1' + f' --prop errBars=stddev' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Standard error with series styling +# +# officecli add charts-scatter.xlsx "/4-Error Bars" --type chart \ +# --prop chartType=scatter \ +# --prop title="Standard Error + Styled Series" \ +# --prop categories=2,4,6,8,10,12,14 \ +# --prop series1="Experiment:18,32,28,45,40,55,52" \ +# --prop colors=843C0B \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop marker=star --prop markerSize=8 \ +# --prop lineWidth=1.5 \ +# --prop errBars=stderr \ +# --prop series.shadow=000000-4-315-2-30 \ +# --prop gridlines=D9D9D9:0.5:dot \ +# --prop catTitle=Time (h) --prop axisTitle=Response +# +# Features: errBars=stderr, series.shadow, gridlines styling +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Error Bars" --type chart' + f' --prop chartType=scatter' + f' --prop title="Standard Error + Styled Series"' + f' --prop categories=2,4,6,8,10,12,14' + f' --prop series1="Experiment:18,32,28,45,40,55,52"' + f' --prop colors=843C0B' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop marker=star --prop markerSize=8' + f' --prop lineWidth=1.5' + f' --prop errBars=stderr' + f' --prop series.shadow=000000-4-315-2-30' + f' --prop gridlines=D9D9D9:0.5:dot' + f' --prop catTitle="Time (h)" --prop axisTitle=Response') + +# ========================================================================== +# Sheet: 5-Styling +# ========================================================================== +print("\n--- 5-Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="5-Styling"') + +# -------------------------------------------------------------------------- +# Chart 1: Title styling, series shadow, series outline +# +# officecli add charts-scatter.xlsx "/5-Styling" --type chart \ +# --prop chartType=scatter \ +# --prop title="Styled Title + Series Effects" \ +# --prop categories=10,20,30,40,50 \ +# --prop series1="Alpha:15,35,28,48,55" \ +# --prop series2="Beta:8,22,32,40,50" \ +# --prop colors=4472C4,ED7D31 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop marker=circle --prop markerSize=8 --prop lineWidth=2 \ +# --prop title.font=Georgia --prop title.size=16 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop title.shadow=000000-3-315-2-30 \ +# --prop series.shadow=000000-4-315-2-30 \ +# --prop series.outline=333333:1.5 \ +# --prop legend=bottom +# +# Features: title.font, title.size, title.color, title.bold, title.shadow, +# series.shadow, series.outline +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Styling" --type chart' + f' --prop chartType=scatter' + f' --prop title="Styled Title + Series Effects"' + f' --prop categories=10,20,30,40,50' + f' --prop series1="Alpha:15,35,28,48,55"' + f' --prop series2="Beta:8,22,32,40,50"' + f' --prop colors=4472C4,ED7D31' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop marker=circle --prop markerSize=8 --prop lineWidth=2' + f' --prop title.font=Georgia --prop title.size=16' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop title.shadow=000000-3-315-2-30' + f' --prop series.shadow=000000-4-315-2-30' + f' --prop series.outline=333333:1.5' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: Gradients, transparency, plotFill, chartFill +# +# officecli add charts-scatter.xlsx "/5-Styling" --type chart \ +# --prop chartType=scatter \ +# --prop title="Gradients + Fills" \ +# --prop categories=5,15,25,35,45 \ +# --prop series1="Group 1:12,28,35,42,55" \ +# --prop series2="Group 2:8,18,22,38,48" \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop marker=diamond --prop markerSize=8 --prop lineWidth=1.5 \ +# --prop 'gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90' \ +# --prop transparency=20 \ +# --prop plotFill=F5F5F5 \ +# --prop chartFill=FAFAFA \ +# --prop legend=bottom +# +# Features: gradients (per-series gradient), transparency, plotFill, chartFill +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Styling" --type chart' + f' --prop chartType=scatter' + f' --prop title="Gradients + Fills"' + f' --prop categories=5,15,25,35,45' + f' --prop series1="Group 1:12,28,35,42,55"' + f' --prop series2="Group 2:8,18,22,38,48"' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop marker=diamond --prop markerSize=8 --prop lineWidth=1.5' + f' --prop "gradients=4472C4-BDD7EE:90;ED7D31-FBE5D6:90"' + f' --prop transparency=20' + f' --prop plotFill=F5F5F5' + f' --prop chartFill=FAFAFA' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Axis font, gridlines, minor gridlines, axis line +# +# officecli add charts-scatter.xlsx "/5-Styling" --type chart \ +# --prop chartType=scatter \ +# --prop title="Axis & Grid Styling" \ +# --prop categories=0,10,20,30,40,50 \ +# --prop series1="Readings:5,22,38,52,68,82" \ +# --prop colors=2E75B6 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop marker=circle --prop markerSize=7 --prop lineWidth=1.5 \ +# --prop axisfont=9:C00000:Arial \ +# --prop gridlines=BFBFBF:0.75:solid \ +# --prop minorGridlines=E0E0E0:0.25:dot \ +# --prop axisLine=333333:1 \ +# --prop catTitle=X Axis --prop axisTitle=Y Axis +# +# Features: axisfont (size:color:font), gridlines, minorGridlines, axisLine +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Styling" --type chart' + f' --prop chartType=scatter' + f' --prop title="Axis & Grid Styling"' + f' --prop categories=0,10,20,30,40,50' + f' --prop series1="Readings:5,22,38,52,68,82"' + f' --prop colors=2E75B6' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop marker=circle --prop markerSize=7 --prop lineWidth=1.5' + f' --prop axisfont=9:C00000:Arial' + f' --prop gridlines=BFBFBF:0.75:solid' + f' --prop minorGridlines=E0E0E0:0.25:dot' + f' --prop axisLine=333333:1' + f' --prop catTitle="X Axis" --prop axisTitle="Y Axis"') + +# -------------------------------------------------------------------------- +# Chart 4: Chart area border, plot area border, rounded corners +# +# officecli add charts-scatter.xlsx "/5-Styling" --type chart \ +# --prop chartType=scatter \ +# --prop title="Borders + Rounded Corners" \ +# --prop categories=1,3,5,7,9 \ +# --prop series1="Data:10,25,18,35,28" \ +# --prop colors=548235 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop marker=square --prop markerSize=8 --prop lineWidth=1.5 \ +# --prop chartArea.border=333333:1.5 \ +# --prop plotArea.border=999999:0.75 \ +# --prop roundedCorners=true \ +# --prop chartFill=FFFFFF \ +# --prop plotFill=F0F0F0 +# +# Features: chartArea.border, plotArea.border, roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/5-Styling" --type chart' + f' --prop chartType=scatter' + f' --prop title="Borders + Rounded Corners"' + f' --prop categories=1,3,5,7,9' + f' --prop series1="Data:10,25,18,35,28"' + f' --prop colors=548235' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop marker=square --prop markerSize=8 --prop lineWidth=1.5' + f' --prop chartArea.border=333333:1.5' + f' --prop plotArea.border=999999:0.75' + f' --prop roundedCorners=true' + f' --prop chartFill=FFFFFF' + f' --prop plotFill=F0F0F0') + +# ========================================================================== +# Sheet: 6-Advanced +# ========================================================================== +print("\n--- 6-Advanced ---") +cli(f'add "{FILE}" / --type sheet --prop name="6-Advanced"') + +# -------------------------------------------------------------------------- +# Chart 1: Secondary axis +# +# officecli add charts-scatter.xlsx "/6-Advanced" --type chart \ +# --prop chartType=scatter \ +# --prop title="Secondary Y-Axis" \ +# --prop categories=10,20,30,40,50,60 \ +# --prop series1="Temperature (C):15,20,28,32,38,42" \ +# --prop series2="Humidity (%):85,78,65,58,45,38" \ +# --prop colors=C00000,4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop marker=circle --prop markerSize=7 --prop lineWidth=1.5 \ +# --prop secondaryAxis=2 \ +# --prop legend=bottom \ +# --prop catTitle=Location +# +# Features: secondaryAxis=2 (series 2 on right Y-axis) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Advanced" --type chart' + f' --prop chartType=scatter' + f' --prop title="Secondary Y-Axis"' + f' --prop categories=10,20,30,40,50,60' + f' --prop series1="Temperature (C):15,20,28,32,38,42"' + f' --prop series2="Humidity (%):85,78,65,58,45,38"' + f' --prop colors=C00000,4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop marker=circle --prop markerSize=7 --prop lineWidth=1.5' + f' --prop secondaryAxis=2' + f' --prop legend=bottom' + f' --prop catTitle=Location') + +# -------------------------------------------------------------------------- +# Chart 2: Reference line (horizontal target) +# +# officecli add charts-scatter.xlsx "/6-Advanced" --type chart \ +# --prop chartType=scatter \ +# --prop title="Reference Line (Target=75)" \ +# --prop categories=1,2,3,4,5,6,7,8 \ +# --prop series1="Score:60,68,72,78,80,74,82,88" \ +# --prop colors=70AD47 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop marker=diamond --prop markerSize=7 --prop lineWidth=1.5 \ +# --prop referenceLine=75:FF0000:Target:dash \ +# --prop catTitle=Week --prop axisTitle=Performance +# +# Features: referenceLine=value:color:label:dash (horizontal target line) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Advanced" --type chart' + f' --prop chartType=scatter' + f' --prop title="Reference Line (Target=75)"' + f' --prop categories=1,2,3,4,5,6,7,8' + f' --prop series1="Score:60,68,72,78,80,74,82,88"' + f' --prop colors=70AD47' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop marker=diamond --prop markerSize=7 --prop lineWidth=1.5' + f' --prop referenceLine=75:FF0000:Target:dash' + f' --prop catTitle=Week --prop axisTitle=Performance') + +# -------------------------------------------------------------------------- +# Chart 3: Axis min/max and log scale +# +# officecli add charts-scatter.xlsx "/6-Advanced" --type chart \ +# --prop chartType=scatter \ +# --prop title="Log Scale (base 10)" \ +# --prop categories=1,10,100,1000,10000 \ +# --prop series1="Response:2,15,120,950,8500" \ +# --prop colors=1F4E79 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop marker=triangle --prop markerSize=8 --prop lineWidth=1.5 \ +# --prop logBase=10 \ +# --prop axisMin=1 --prop axisMax=10000 \ +# --prop catTitle=Concentration --prop axisTitle=Response +# +# Features: logBase=10 (logarithmic value axis), axisMin, axisMax +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Advanced" --type chart' + f' --prop chartType=scatter' + f' --prop title="Log Scale (base 10)"' + f' --prop categories=1,10,100,1000,10000' + f' --prop series1="Response:2,15,120,950,8500"' + f' --prop colors=1F4E79' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop marker=triangle --prop markerSize=8 --prop lineWidth=1.5' + f' --prop logBase=10' + f' --prop axisMin=1 --prop axisMax=10000' + f' --prop catTitle=Concentration --prop axisTitle=Response') + +# -------------------------------------------------------------------------- +# Chart 4: Data labels and color rule +# +# officecli add charts-scatter.xlsx "/6-Advanced" --type chart \ +# --prop chartType=scatter \ +# --prop scatterStyle=marker \ +# --prop title="Data Labels + Color Rule" \ +# --prop categories=1,2,3,4,5,6,7,8 \ +# --prop series1="KPI:45,62,38,78,55,82,48,90" \ +# --prop colors=4472C4 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop markerSize=9 \ +# --prop dataLabels=true --prop labelPos=top \ +# --prop colorRule=60:C00000:00AA00 \ +# --prop catTitle=Quarter --prop axisTitle=KPI Score +# +# Features: dataLabels=true, labelPos=top, colorRule=threshold:below:above +# (points below 60 = red, above = green) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/6-Advanced" --type chart' + f' --prop chartType=scatter' + f' --prop scatterStyle=marker' + f' --prop title="Data Labels + Color Rule"' + f' --prop categories=1,2,3,4,5,6,7,8' + f' --prop series1="KPI:45,62,38,78,55,82,48,90"' + f' --prop colors=4472C4' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop markerSize=9' + f' --prop dataLabels=true --prop labelPos=top' + f' --prop colorRule=60:C00000:00AA00' + f' --prop catTitle=Quarter --prop axisTitle="KPI Score"') + +# Remove blank default Sheet1 (all data is inline) +cli(f'remove "{FILE}" /Sheet1') + +print(f"\nDone! Generated: {FILE}") +print(" 7 sheets (6 chart sheets, 24 charts total)") diff --git a/examples/excel/charts/charts-scatter.xlsx b/examples/excel/charts/charts-scatter.xlsx new file mode 100644 index 000000000..804ca2328 Binary files /dev/null and b/examples/excel/charts/charts-scatter.xlsx differ diff --git a/examples/excel/charts/charts-stock.md b/examples/excel/charts/charts-stock.md new file mode 100644 index 000000000..26863736c --- /dev/null +++ b/examples/excel/charts/charts-stock.md @@ -0,0 +1,117 @@ +# Stock Charts Showcase + +This demo consists of three files that work together: + +- **charts-stock.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-stock.xlsx** — The generated workbook with 4 sheets (1 default + 3 chart sheets, 12 charts total). +- **charts-stock.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-stock.py +# -> charts-stock.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Stock Fundamentals + +Four OHLC stock charts covering basic rendering, gridlines, hi-low lines, and up-down bars. + +```bash +# Basic OHLC stock chart with axis titles +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop series1="Open:142,145,148,150,147,152" \ + --prop series2="High:148,151,155,156,153,158" \ + --prop series3="Low:139,142,145,147,144,149" \ + --prop series4="Close:145,148,150,147,152,155" \ + --prop catTitle=Week --prop axisTitle=Price ($) + +# Stock with gridlines and axis font +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop gridlines=D9D9D9:0.5 --prop axisfont=9:666666 + +# Hi-low lines connecting high to low per category +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop hiLowLines=true + +# Up-down bars showing open-to-close direction +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop updownbars=100:70AD47:C00000 +``` + +**Features:** `stock`, 4-series OHLC, `catTitle`, `axisTitle`, `gridlines`, `axisfont`, `hiLowLines`, `updownbars=gapWidth:upColor:downColor` + +### Sheet: 2-Stock Styling + +Four styled stock charts with title fonts, axis lines, custom ranges, and chart fills. + +```bash +# Title and legend styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop title.font=Georgia --prop title.size=16 \ + --prop title.color=1F4E79 --prop title.bold=true \ + --prop legend=right --prop legendfont=10:333333:Calibri + +# Axis line styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop axisLine=333333-1.5 --prop catAxisLine=333333-1.5 + +# Custom axis range with major unit +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop axisMin=110 --prop axisMax=150 --prop majorUnit=10 + +# Chart area fills and rounded corners +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop plotFill=F0F4F8 --prop chartFill=FAFAFA \ + --prop roundedCorners=true +``` + +**Features:** `title.font/size/color/bold`, `legend=right`, `legendfont`, `axisLine`, `catAxisLine`, `axisMin/Max`, `majorUnit`, `plotFill`, `chartFill`, `roundedCorners` + +### Sheet: 3-Stock Advanced + +Four advanced stock charts with data labels, reference lines, borders, and number formatting. + +```bash +# Data labels on stock chart +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop dataLabels=true --prop labelPos=top \ + --prop labelFont=8:666666:false + +# Reference line as support/resistance level +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop referenceLine=115:Resistance:C00000 + +# Chart and plot area borders +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop chartArea.border=333333-1.5 \ + --prop plotArea.border=999999-0.75 + +# Axis number format (dollar) +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=stock \ + --prop axisNumFmt=$#,##0 +``` + +**Features:** `dataLabels`, `labelPos`, `labelFont`, `referenceLine`, `chartArea.border`, `plotArea.border`, `axisNumFmt` + +## Inspect the Generated File + +```bash +officecli query charts-stock.xlsx chart +officecli get charts-stock.xlsx "/1-Stock Fundamentals/chart[1]" +``` diff --git a/examples/excel/charts/charts-stock.py b/examples/excel/charts/charts-stock.py new file mode 100644 index 000000000..ea209c005 --- /dev/null +++ b/examples/excel/charts/charts-stock.py @@ -0,0 +1,429 @@ +#!/usr/bin/env python3 +""" +Stock Charts Showcase — OHLC with hi-low lines, up-down bars, and styling. + +Generates: charts-stock.xlsx + +Usage: + python3 charts-stock.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "charts-stock.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Sheet: 1-Stock Fundamentals +# ========================================================================== +print("\n--- 1-Stock Fundamentals ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Stock Fundamentals"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic OHLC stock chart +# +# officecli add charts-stock.xlsx "/1-Stock Fundamentals" --type chart \ +# --prop chartType=stock \ +# --prop title="ACME Corp Weekly OHLC" \ +# --prop series1="Open:142,145,148,150,147,152" \ +# --prop series2="High:148,151,155,156,153,158" \ +# --prop series3="Low:139,142,145,147,144,149" \ +# --prop series4="Close:145,148,150,147,152,155" \ +# --prop categories=Week 1,Week 2,Week 3,Week 4,Week 5,Week 6 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop catTitle=Week --prop axisTitle=Price ($) \ +# --prop legend=bottom +# +# Features: chartType=stock, 4 series (Open/High/Low/Close), catTitle, axisTitle +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Stock Fundamentals" --type chart' + f' --prop chartType=stock' + f' --prop title="ACME Corp Weekly OHLC"' + f' --prop series1=Open:142,145,148,150,147,152' + f' --prop series2=High:148,151,155,156,153,158' + f' --prop series3=Low:139,142,145,147,144,149' + f' --prop series4=Close:145,148,150,147,152,155' + f' --prop "categories=Week 1,Week 2,Week 3,Week 4,Week 5,Week 6"' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop catTitle=Week --prop "axisTitle=Price ($)"' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: Stock with gridlines and axisfont +# +# officecli add charts-stock.xlsx "/1-Stock Fundamentals" --type chart \ +# --prop chartType=stock \ +# --prop title="Tech Sector Daily" \ +# --prop series1="Open:210,215,212,218,220" \ +# --prop series2="High:218,222,219,225,228" \ +# --prop series3="Low:207,211,208,214,216" \ +# --prop series4="Close:215,212,218,220,225" \ +# --prop categories=Mon,Tue,Wed,Thu,Fri \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop gridlines=D9D9D9:0.5 \ +# --prop axisfont=9:666666 \ +# --prop legend=bottom +# +# Features: gridlines, axisfont on stock chart +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Stock Fundamentals" --type chart' + f' --prop chartType=stock' + f' --prop title="Tech Sector Daily"' + f' --prop series1=Open:210,215,212,218,220' + f' --prop series2=High:218,222,219,225,228' + f' --prop series3=Low:207,211,208,214,216' + f' --prop series4=Close:215,212,218,220,225' + f' --prop categories=Mon,Tue,Wed,Thu,Fri' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop gridlines=D9D9D9:0.5' + f' --prop axisfont=9:666666' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Stock with hiLowLines +# +# officecli add charts-stock.xlsx "/1-Stock Fundamentals" --type chart \ +# --prop chartType=stock \ +# --prop title="Energy Sector with Hi-Low Lines" \ +# --prop series1="Open:78,80,82,79,83,85" \ +# --prop series2="High:84,86,88,85,89,91" \ +# --prop series3="Low:75,77,79,76,80,82" \ +# --prop series4="Close:80,82,79,83,85,88" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop hiLowLines=true \ +# --prop legend=bottom +# +# Features: hiLowLines=true (vertical lines connecting high to low) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Stock Fundamentals" --type chart' + f' --prop chartType=stock' + f' --prop title="Energy Sector with Hi-Low Lines"' + f' --prop series1=Open:78,80,82,79,83,85' + f' --prop series2=High:84,86,88,85,89,91' + f' --prop series3=Low:75,77,79,76,80,82' + f' --prop series4=Close:80,82,79,83,85,88' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop hiLowLines=true' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: Stock with updownbars +# +# officecli add charts-stock.xlsx "/1-Stock Fundamentals" --type chart \ +# --prop chartType=stock \ +# --prop title="Pharma Index with Up-Down Bars" \ +# --prop series1="Open:55,58,56,60,62,59" \ +# --prop series2="High:61,63,62,66,68,65" \ +# --prop series3="Low:52,55,53,57,59,56" \ +# --prop series4="Close:58,56,60,62,59,63" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop updownbars=100:70AD47:C00000 \ +# --prop legend=bottom +# +# Features: updownbars=gapWidth:upColor:downColor +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Stock Fundamentals" --type chart' + f' --prop chartType=stock' + f' --prop title="Pharma Index with Up-Down Bars"' + f' --prop series1=Open:55,58,56,60,62,59' + f' --prop series2=High:61,63,62,66,68,65' + f' --prop series3=Low:52,55,53,57,59,56' + f' --prop series4=Close:58,56,60,62,59,63' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop updownbars=100:70AD47:C00000' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 2-Stock Styling +# ========================================================================== +print("\n--- 2-Stock Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Stock Styling"') + +# -------------------------------------------------------------------------- +# Chart 1: Title styling, legend positioning +# +# officecli add charts-stock.xlsx "/2-Stock Styling" --type chart \ +# --prop chartType=stock \ +# --prop title="Styled Stock Chart" \ +# --prop series1="Open:165,170,168,172,175" \ +# --prop series2="High:175,178,176,180,183" \ +# --prop series3="Low:160,165,163,168,170" \ +# --prop series4="Close:170,168,172,175,180" \ +# --prop categories=Mon,Tue,Wed,Thu,Fri \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop title.font=Georgia --prop title.size=16 \ +# --prop title.color=1F4E79 --prop title.bold=true \ +# --prop legend=right --prop legendfont=10:333333:Calibri +# +# Features: title.font/size/color/bold, legend=right, legendfont +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Stock Styling" --type chart' + f' --prop chartType=stock' + f' --prop title="Styled Stock Chart"' + f' --prop series1=Open:165,170,168,172,175' + f' --prop series2=High:175,178,176,180,183' + f' --prop series3=Low:160,165,163,168,170' + f' --prop series4=Close:170,168,172,175,180' + f' --prop categories=Mon,Tue,Wed,Thu,Fri' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop title.font=Georgia --prop title.size=16' + f' --prop title.color=1F4E79 --prop title.bold=true' + f' --prop legend=right --prop legendfont=10:333333:Calibri') + +# -------------------------------------------------------------------------- +# Chart 2: Series effects, axisLine, catAxisLine +# +# officecli add charts-stock.xlsx "/2-Stock Styling" --type chart \ +# --prop chartType=stock \ +# --prop title="Axis Line Styling" \ +# --prop series1="Open:92,95,93,97,99" \ +# --prop series2="High:99,102,100,104,106" \ +# --prop series3="Low:88,91,89,93,95" \ +# --prop series4="Close:95,93,97,99,103" \ +# --prop categories=W1,W2,W3,W4,W5 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop hiLowLines=true \ +# --prop axisLine=333333:1.5 --prop catAxisLine=333333:1.5 \ +# --prop legend=bottom +# +# Features: axisLine, catAxisLine on stock chart +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Stock Styling" --type chart' + f' --prop chartType=stock' + f' --prop title="Axis Line Styling"' + f' --prop series1=Open:92,95,93,97,99' + f' --prop series2=High:99,102,100,104,106' + f' --prop series3=Low:88,91,89,93,95' + f' --prop series4=Close:95,93,97,99,103' + f' --prop categories=W1,W2,W3,W4,W5' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop hiLowLines=true' + f' --prop axisLine=333333:1.5 --prop catAxisLine=333333:1.5' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: axisMin/Max, majorUnit +# +# officecli add charts-stock.xlsx "/2-Stock Styling" --type chart \ +# --prop chartType=stock \ +# --prop title="Custom Axis Range" \ +# --prop series1="Open:120,125,122,128,130" \ +# --prop series2="High:132,138,135,140,142" \ +# --prop series3="Low:115,120,118,124,126" \ +# --prop series4="Close:125,122,128,130,135" \ +# --prop categories=Day 1,Day 2,Day 3,Day 4,Day 5 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop axisMin=110 --prop axisMax=150 \ +# --prop majorUnit=10 \ +# --prop updownbars=100:70AD47:C00000 \ +# --prop legend=bottom +# +# Features: axisMin/Max, majorUnit +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Stock Styling" --type chart' + f' --prop chartType=stock' + f' --prop title="Custom Axis Range"' + f' --prop series1=Open:120,125,122,128,130' + f' --prop series2=High:132,138,135,140,142' + f' --prop series3=Low:115,120,118,124,126' + f' --prop series4=Close:125,122,128,130,135' + f' --prop "categories=Day 1,Day 2,Day 3,Day 4,Day 5"' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop axisMin=110 --prop axisMax=150' + f' --prop majorUnit=10' + f' --prop updownbars=100:70AD47:C00000' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: plotFill, chartFill, roundedCorners +# +# officecli add charts-stock.xlsx "/2-Stock Styling" --type chart \ +# --prop chartType=stock \ +# --prop title="Styled Chart Area" \ +# --prop series1="Open:48,50,52,49,53" \ +# --prop series2="High:55,57,59,56,60" \ +# --prop series3="Low:44,46,48,45,49" \ +# --prop series4="Close:50,52,49,53,56" \ +# --prop categories=Mon,Tue,Wed,Thu,Fri \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop plotFill=F0F4F8 --prop chartFill=FAFAFA \ +# --prop roundedCorners=true \ +# --prop hiLowLines=true \ +# --prop legend=bottom +# +# Features: plotFill, chartFill, roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Stock Styling" --type chart' + f' --prop chartType=stock' + f' --prop title="Styled Chart Area"' + f' --prop series1=Open:48,50,52,49,53' + f' --prop series2=High:55,57,59,56,60' + f' --prop series3=Low:44,46,48,45,49' + f' --prop series4=Close:50,52,49,53,56' + f' --prop categories=Mon,Tue,Wed,Thu,Fri' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop plotFill=F0F4F8 --prop chartFill=FAFAFA' + f' --prop roundedCorners=true' + f' --prop hiLowLines=true' + f' --prop legend=bottom') + +# ========================================================================== +# Sheet: 3-Stock Advanced +# ========================================================================== +print("\n--- 3-Stock Advanced ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Stock Advanced"') + +# -------------------------------------------------------------------------- +# Chart 1: dataLabels, labelFont +# +# officecli add charts-stock.xlsx "/3-Stock Advanced" --type chart \ +# --prop chartType=stock \ +# --prop title="Stock with Data Labels" \ +# --prop series1="Open:185,190,188,192,195" \ +# --prop series2="High:195,198,196,200,203" \ +# --prop series3="Low:180,185,183,188,190" \ +# --prop series4="Close:190,188,192,195,200" \ +# --prop categories=W1,W2,W3,W4,W5 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop dataLabels=true --prop labelPos=top \ +# --prop labelFont=8:666666:false \ +# --prop legend=bottom +# +# Features: dataLabels, labelPos, labelFont on stock +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Stock Advanced" --type chart' + f' --prop chartType=stock' + f' --prop title="Stock with Data Labels"' + f' --prop series1=Open:185,190,188,192,195' + f' --prop series2=High:195,198,196,200,203' + f' --prop series3=Low:180,185,183,188,190' + f' --prop series4=Close:190,188,192,195,200' + f' --prop categories=W1,W2,W3,W4,W5' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop dataLabels=true --prop labelPos=top' + f' --prop labelFont=8:666666:false' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 2: referenceLine (support/resistance) +# +# officecli add charts-stock.xlsx "/3-Stock Advanced" --type chart \ +# --prop chartType=stock \ +# --prop title="Support & Resistance" \ +# --prop series1="Open:105,108,106,110,112,109" \ +# --prop series2="High:112,115,113,117,119,116" \ +# --prop series3="Low:101,104,102,106,108,105" \ +# --prop series4="Close:108,106,110,112,109,113" \ +# --prop categories=Jan,Feb,Mar,Apr,May,Jun \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop referenceLine=115:C00000:Resistance \ +# --prop hiLowLines=true \ +# --prop legend=bottom +# +# Features: referenceLine as support/resistance level +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Stock Advanced" --type chart' + f' --prop chartType=stock' + f' --prop title="Support & Resistance"' + f' --prop series1=Open:105,108,106,110,112,109' + f' --prop series2=High:112,115,113,117,119,116' + f' --prop series3=Low:101,104,102,106,108,105' + f' --prop series4=Close:108,106,110,112,109,113' + f' --prop categories=Jan,Feb,Mar,Apr,May,Jun' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop referenceLine=115:C00000:Resistance' + f' --prop hiLowLines=true' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: chartArea.border, plotArea.border +# +# officecli add charts-stock.xlsx "/3-Stock Advanced" --type chart \ +# --prop chartType=stock \ +# --prop title="Bordered Stock Chart" \ +# --prop series1="Open:72,75,73,77,79" \ +# --prop series2="High:79,82,80,84,86" \ +# --prop series3="Low:68,71,69,73,75" \ +# --prop series4="Close:75,73,77,79,83" \ +# --prop categories=Mon,Tue,Wed,Thu,Fri \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop chartArea.border=333333:1.5 \ +# --prop plotArea.border=999999:0.75 \ +# --prop updownbars=100:70AD47:C00000 \ +# --prop legend=bottom +# +# Features: chartArea.border, plotArea.border +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Stock Advanced" --type chart' + f' --prop chartType=stock' + f' --prop title="Bordered Stock Chart"' + f' --prop series1=Open:72,75,73,77,79' + f' --prop series2=High:79,82,80,84,86' + f' --prop series3=Low:68,71,69,73,75' + f' --prop series4=Close:75,73,77,79,83' + f' --prop categories=Mon,Tue,Wed,Thu,Fri' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop chartArea.border=333333:1.5' + f' --prop plotArea.border=999999:0.75' + f' --prop updownbars=100:70AD47:C00000' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 4: dispUnits, axisNumFmt +# +# officecli add charts-stock.xlsx "/3-Stock Advanced" --type chart \ +# --prop chartType=stock \ +# --prop title="Large Cap Stock" \ +# --prop series1="Open:2850,2900,2880,2920,2950" \ +# --prop series2="High:2950,2980,2960,3000,3020" \ +# --prop series3="Low:2800,2850,2830,2870,2900" \ +# --prop series4="Close:2900,2880,2920,2950,2990" \ +# --prop categories=Q1,Q2,Q3,Q4,Q5 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop axisNumFmt=$#,##0 \ +# --prop hiLowLines=true \ +# --prop legend=bottom +# +# Features: axisNumFmt (dollar format) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Stock Advanced" --type chart' + f' --prop chartType=stock' + f' --prop title="Large Cap Stock"' + f' --prop series1=Open:2850,2900,2880,2920,2950' + f' --prop series2=High:2950,2980,2960,3000,3020' + f' --prop series3=Low:2800,2850,2830,2870,2900' + f' --prop series4=Close:2900,2880,2920,2950,2990' + f' --prop categories=Q1,Q2,Q3,Q4,Q5' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop "axisNumFmt=$#,##0"' + f' --prop hiLowLines=true' + f' --prop legend=bottom') + +# Remove blank default Sheet1 (all data is inline) +cli(f'remove "{FILE}" /Sheet1') + +print(f"\nDone! Generated: {FILE}") +print(" 4 sheets (3 chart sheets, 12 charts total)") diff --git a/examples/excel/charts/charts-stock.xlsx b/examples/excel/charts/charts-stock.xlsx new file mode 100644 index 000000000..2782ffff9 Binary files /dev/null and b/examples/excel/charts/charts-stock.xlsx differ diff --git a/examples/excel/charts/charts-waterfall.md b/examples/excel/charts/charts-waterfall.md new file mode 100644 index 000000000..6434cb488 --- /dev/null +++ b/examples/excel/charts/charts-waterfall.md @@ -0,0 +1,189 @@ +# Waterfall Charts Showcase + +This demo consists of three files that work together: + +- **charts-waterfall.py** — Python script that calls `officecli` commands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. +- **charts-waterfall.xlsx** — The generated workbook with 5 sheets (1 default + 4 chart sheets, 16 charts total). +- **charts-waterfall.md** — This file. Maps each sheet to the features it demonstrates. + +## Regenerate + +```bash +cd examples/excel +python3 charts-waterfall.py +# → charts-waterfall.xlsx +``` + +## Chart Sheets + +### Sheet: 1-Waterfall Fundamentals + +Four waterfall chart variants covering basic P&L, budget analysis, quarterly flow, and title styling. + +```bash +# Basic P&L waterfall with increase/decrease/total colors +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop data="Start:1000,Revenue:500,Costs:-300,Tax:-100,Net:1100" \ + --prop increaseColor=70AD47 \ + --prop decreaseColor=FF0000 \ + --prop totalColor=4472C4 \ + --prop dataLabels=true + +# Budget waterfall with blue/red/amber theme +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop data="Budget:5000,Sales:2000,Marketing:-800,Ops:-600,Net:5600" \ + --prop increaseColor=2E75B6 \ + --prop decreaseColor=C00000 \ + --prop totalColor=FFC000 \ + --prop legend=bottom + +# Quarterly cash flow with 10 data points +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop data="Opening:3000,Q1 Sales:1200,Q1 Costs:-500,...,Closing:6000" \ + --prop increaseColor=70AD47 --prop decreaseColor=ED7D31 --prop totalColor=4472C4 + +# Waterfall with styled title +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop title.font=Georgia --prop title.size=16 \ + --prop title.color=1F4E79 --prop title.bold=true +``` + +**Features:** `chartType=waterfall`, `data=` name:value pairs (positive=increase, negative=decrease), `increaseColor`, `decreaseColor`, `totalColor`, `dataLabels`, `legend=bottom`, `title.font/size/color/bold` + +### Sheet: 2-Waterfall Styling + +Four waterfall charts demonstrating visual styling options. + +```bash +# Title with font, size, color, bold, and shadow +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop title.font=Trebuchet MS --prop title.size=18 \ + --prop title.color=833C0B --prop title.bold=true \ + --prop title.shadow=000000-3-315-2-30 + +# Series shadow, plot/chart fills, rounded corners +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop series.shadow=000000-4-315-2-30 \ + --prop plotFill=F0F0F0 --prop chartFill=FAFAFA \ + --prop roundedCorners=true + +# Gridline color and axis font +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop gridlineColor=CCCCCC \ + --prop axisfont=10:333333:Calibri + +# Chart area and plot area borders +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop chartArea.border=4472C4-2 \ + --prop plotArea.border=A5A5A5-1 +``` + +**Features:** `title.shadow`, `series.shadow`, `plotFill`, `chartFill`, `roundedCorners`, `gridlineColor`, `axisfont`, `chartArea.border`, `plotArea.border` + +### Sheet: 3-Waterfall Labels & Axis + +Four waterfall charts demonstrating data labels, axis configuration, and layout control. + +```bash +# Data labels with font and number format +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop dataLabels=true \ + --prop labelFont=10:333333:true \ + --prop dataLabels.numFmt=#,##0 + +# Custom axis range and tick interval +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop axisMin=0 --prop axisMax=3500 --prop majorUnit=500 + +# Legend position and font +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop legend=right \ + --prop legendfont=10:1F4E79:Helvetica + +# Manual plot area layout +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop plotArea.x=0.15 --prop plotArea.y=0.15 \ + --prop plotArea.w=0.75 --prop plotArea.h=0.70 +``` + +**Features:** `dataLabels`, `labelFont`, `dataLabels.numFmt`, `axisMin`, `axisMax`, `majorUnit`, `legend=right`, `legendfont`, `plotArea.x/y/w/h` + +### Sheet: 4-Waterfall Advanced + +Four waterfall charts demonstrating advanced features and large datasets. + +```bash +# Reference line overlay +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop referenceLine=2000:Target-FF0000-dash-2 + +# Value axis and category axis line styling +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop axisLine=333333-2 \ + --prop catAxisLine=333333-2 + +# Title glow and shadow effects +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop title.glow=4472C4-8 \ + --prop title.shadow=000000-3-315-2-30 + +# Large dataset (12 categories) with small axis font +officecli add data.xlsx /Sheet --type chart \ + --prop chartType=waterfall \ + --prop data="Revenue:8500,COGS:-3400,...,Net Income:1050" \ + --prop dataLabels=true \ + --prop axisfont=8:333333:Calibri +``` + +**Features:** `referenceLine`, `axisLine`, `catAxisLine`, `title.glow`, `title.shadow`, large dataset (12 categories) + +## Property Coverage + +| Property | Sheet | +|---|---| +| `chartType=waterfall` | 1, 2, 3, 4 | +| `data=` (name:value pairs) | 1, 2, 3, 4 | +| `increaseColor` | 1, 2, 3, 4 | +| `decreaseColor` | 1, 2, 3, 4 | +| `totalColor` | 1, 2, 3, 4 | +| `dataLabels` | 1, 3, 4 | +| `legend` | 1, 3 | +| `title.font/size/color/bold` | 1, 2 | +| `title.shadow` | 2, 4 | +| `title.glow` | 4 | +| `series.shadow` | 2 | +| `plotFill`, `chartFill` | 2 | +| `roundedCorners` | 2 | +| `gridlineColor` | 2 | +| `axisfont` | 2, 4 | +| `chartArea.border` | 2 | +| `plotArea.border` | 2 | +| `labelFont` | 3 | +| `dataLabels.numFmt` | 3 | +| `axisMin/Max`, `majorUnit` | 3 | +| `legendfont` | 3 | +| `plotArea.x/y/w/h` | 3 | +| `referenceLine` | 4 | +| `axisLine`, `catAxisLine` | 4 | + +## Inspect the Generated File + +```bash +officecli query charts-waterfall.xlsx chart +officecli get charts-waterfall.xlsx "/1-Waterfall Fundamentals/chart[1]" +``` diff --git a/examples/excel/charts/charts-waterfall.py b/examples/excel/charts/charts-waterfall.py new file mode 100644 index 000000000..db74e765c --- /dev/null +++ b/examples/excel/charts/charts-waterfall.py @@ -0,0 +1,502 @@ +#!/usr/bin/env python3 +""" +Waterfall Charts Showcase — waterfall chart type with all variations. + +Generates: charts-waterfall.xlsx + +Usage: + python3 charts-waterfall.py +""" + +import subprocess, sys, os, atexit + +FILE = "charts-waterfall.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Sheet: 1-Waterfall Fundamentals +# ========================================================================== +print("\n--- 1-Waterfall Fundamentals ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Waterfall Fundamentals"') + +# -------------------------------------------------------------------------- +# Chart 1: Basic P&L waterfall with increase/decrease/total colors +# +# officecli add charts-waterfall.xlsx "/1-Waterfall Fundamentals" --type chart \ +# --prop chartType=waterfall \ +# --prop title="P&L Summary" \ +# --prop data="Start:1000,Revenue:500,Costs:-300,Tax:-100,Net:1100" \ +# --prop increaseColor=70AD47 \ +# --prop decreaseColor=FF0000 \ +# --prop totalColor=4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop dataLabels=true +# +# Features: chartType=waterfall, data= name:value pairs, increaseColor, +# decreaseColor, totalColor, dataLabels +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Waterfall Fundamentals" --type chart' + f' --prop chartType=waterfall' + f' --prop title="P&L Summary"' + f' --prop data=Start:1000,Revenue:500,Costs:-300,Tax:-100,Net:1100' + f' --prop increaseColor=70AD47' + f' --prop decreaseColor=FF0000' + f' --prop totalColor=4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop dataLabels=true') + +# -------------------------------------------------------------------------- +# Chart 2: Budget waterfall with blue/red/amber theme and legend +# +# officecli add charts-waterfall.xlsx "/1-Waterfall Fundamentals" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Budget vs Actual" \ +# --prop data="Budget:5000,Sales:2000,Marketing:-800,Ops:-600,Net:5600" \ +# --prop increaseColor=2E75B6 \ +# --prop decreaseColor=C00000 \ +# --prop totalColor=FFC000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop legend=bottom +# +# Features: waterfall legend=bottom, alternative color palette (blue/red/amber) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Waterfall Fundamentals" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Budget vs Actual"' + f' --prop data=Budget:5000,Sales:2000,Marketing:-800,Ops:-600,Net:5600' + f' --prop increaseColor=2E75B6' + f' --prop decreaseColor=C00000' + f' --prop totalColor=FFC000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop legend=bottom') + +# -------------------------------------------------------------------------- +# Chart 3: Quarterly cash flow bridge with more data points +# +# officecli add charts-waterfall.xlsx "/1-Waterfall Fundamentals" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Quarterly Cash Flow" \ +# --prop data="Opening:3000,Q1 Sales:1200,Q1 Costs:-500,Q2 Sales:1500,Q2 Costs:-700,Q3 Sales:800,Q3 Costs:-400,Q4 Sales:2000,Q4 Costs:-900,Closing:6000" \ +# --prop increaseColor=70AD47 \ +# --prop decreaseColor=ED7D31 \ +# --prop totalColor=4472C4 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop dataLabels=true +# +# Features: waterfall with 10 categories (extended data points), +# quarterly granularity +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Waterfall Fundamentals" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Quarterly Cash Flow"' + f' --prop "data=Opening:3000,Q1 Sales:1200,Q1 Costs:-500,Q2 Sales:1500,Q2 Costs:-700,Q3 Sales:800,Q3 Costs:-400,Q4 Sales:2000,Q4 Costs:-900,Closing:6000"' + f' --prop increaseColor=70AD47' + f' --prop decreaseColor=ED7D31' + f' --prop totalColor=4472C4' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop dataLabels=true') + +# -------------------------------------------------------------------------- +# Chart 4: Waterfall with custom title styling +# +# officecli add charts-waterfall.xlsx "/1-Waterfall Fundamentals" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Revenue Bridge" \ +# --prop data="Base:2500,New Clients:800,Upsell:400,Churn:-600,Total:3100" \ +# --prop increaseColor=548235 \ +# --prop decreaseColor=BF0000 \ +# --prop totalColor=2F5496 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop title.font=Georgia --prop title.size=16 \ +# --prop title.color=1F4E79 --prop title.bold=true +# +# Features: title.font, title.size, title.color, title.bold +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/1-Waterfall Fundamentals" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Revenue Bridge"' + f' --prop "data=Base:2500,New Clients:800,Upsell:400,Churn:-600,Total:3100"' + f' --prop increaseColor=548235' + f' --prop decreaseColor=BF0000' + f' --prop totalColor=2F5496' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop title.font=Georgia --prop title.size=16' + f' --prop title.color=1F4E79 --prop title.bold=true') + +# ========================================================================== +# Sheet: 2-Waterfall Styling +# ========================================================================== +print("\n--- 2-Waterfall Styling ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Waterfall Styling"') + +# -------------------------------------------------------------------------- +# Chart 1: Title styling with font, size, color, bold, and shadow +# +# officecli add charts-waterfall.xlsx "/2-Waterfall Styling" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Styled Title Demo" \ +# --prop data="Start:800,Income:300,Expenses:-200,Net:900" \ +# --prop increaseColor=70AD47 \ +# --prop decreaseColor=FF0000 \ +# --prop totalColor=4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop title.font=Trebuchet MS --prop title.size=18 \ +# --prop title.color=833C0B --prop title.bold=true \ +# --prop title.shadow=000000-3-315-2-30 +# +# Features: title.font, title.size, title.color, title.bold, title.shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Waterfall Styling" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Styled Title Demo"' + f' --prop data=Start:800,Income:300,Expenses:-200,Net:900' + f' --prop increaseColor=70AD47' + f' --prop decreaseColor=FF0000' + f' --prop totalColor=4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop "title.font=Trebuchet MS" --prop title.size=18' + f' --prop title.color=833C0B --prop title.bold=true' + f' --prop title.shadow=000000-3-315-2-30') + +# -------------------------------------------------------------------------- +# Chart 2: Series shadow, plotFill, chartFill, roundedCorners +# +# officecli add charts-waterfall.xlsx "/2-Waterfall Styling" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Shadow & Fill Effects" \ +# --prop data="Baseline:1500,Growth:600,Decline:-400,Result:1700" \ +# --prop increaseColor=2E75B6 \ +# --prop decreaseColor=C00000 \ +# --prop totalColor=FFC000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop series.shadow=000000-4-315-2-30 \ +# --prop plotFill=F0F0F0 \ +# --prop chartFill=FAFAFA \ +# --prop roundedCorners=true +# +# Features: series.shadow, plotFill, chartFill, roundedCorners +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Waterfall Styling" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Shadow & Fill Effects"' + f' --prop data=Baseline:1500,Growth:600,Decline:-400,Result:1700' + f' --prop increaseColor=2E75B6' + f' --prop decreaseColor=C00000' + f' --prop totalColor=FFC000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop series.shadow=000000-4-315-2-30' + f' --prop plotFill=F0F0F0' + f' --prop chartFill=FAFAFA' + f' --prop roundedCorners=true') + +# -------------------------------------------------------------------------- +# Chart 3: Gridlines styling and axis font +# +# officecli add charts-waterfall.xlsx "/2-Waterfall Styling" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Gridlines & Axis Font" \ +# --prop data="Open:2000,Add:750,Remove:-350,Close:2400" \ +# --prop increaseColor=70AD47 \ +# --prop decreaseColor=FF0000 \ +# --prop totalColor=4472C4 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop gridlineColor=CCCCCC \ +# --prop axisfont=10:333333:Calibri +# +# Features: gridlineColor, axisfont (size:color:font) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Waterfall Styling" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Gridlines & Axis Font"' + f' --prop data=Open:2000,Add:750,Remove:-350,Close:2400' + f' --prop increaseColor=70AD47' + f' --prop decreaseColor=FF0000' + f' --prop totalColor=4472C4' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop gridlineColor=CCCCCC' + f' --prop axisfont=10:333333:Calibri') + +# -------------------------------------------------------------------------- +# Chart 4: Chart area border and plot area border +# +# officecli add charts-waterfall.xlsx "/2-Waterfall Styling" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Border Styling" \ +# --prop data="Initial:1200,Gain:500,Loss:-300,Final:1400" \ +# --prop increaseColor=548235 \ +# --prop decreaseColor=BF0000 \ +# --prop totalColor=2F5496 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop chartArea.border=4472C4:2 \ +# --prop plotArea.border=A5A5A5:1 +# +# Features: chartArea.border (color-width), plotArea.border +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/2-Waterfall Styling" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Border Styling"' + f' --prop data=Initial:1200,Gain:500,Loss:-300,Final:1400' + f' --prop increaseColor=548235' + f' --prop decreaseColor=BF0000' + f' --prop totalColor=2F5496' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop chartArea.border=4472C4:2' + f' --prop plotArea.border=A5A5A5:1') + +# ========================================================================== +# Sheet: 3-Waterfall Labels & Axis +# ========================================================================== +print("\n--- 3-Waterfall Labels & Axis ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Waterfall Labels & Axis"') + +# -------------------------------------------------------------------------- +# Chart 1: Data labels with labelFont and numFmt +# +# officecli add charts-waterfall.xlsx "/3-Waterfall Labels & Axis" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Labels with NumFmt" \ +# --prop data="Start:4500,Revenue:1800,COGS:-1200,SGA:-600,Net:4500" \ +# --prop increaseColor=70AD47 \ +# --prop decreaseColor=FF0000 \ +# --prop totalColor=4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop dataLabels=true \ +# --prop labelFont=10:333333:true \ +# --prop dataLabels.numFmt=#,##0 +# +# Features: dataLabels, labelFont (size:color:bold), dataLabels.numFmt +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Waterfall Labels & Axis" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Labels with NumFmt"' + f' --prop data=Start:4500,Revenue:1800,COGS:-1200,SGA:-600,Net:4500' + f' --prop increaseColor=70AD47' + f' --prop decreaseColor=FF0000' + f' --prop totalColor=4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop dataLabels=true' + f' --prop labelFont=10:333333:true' + f' --prop dataLabels.numFmt=#,##0') + +# -------------------------------------------------------------------------- +# Chart 2: Axis min/max and majorUnit +# +# officecli add charts-waterfall.xlsx "/3-Waterfall Labels & Axis" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Custom Axis Range" \ +# --prop data="Base:2000,Up:800,Down:-500,Total:2300" \ +# --prop increaseColor=2E75B6 \ +# --prop decreaseColor=C00000 \ +# --prop totalColor=FFC000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop axisMin=0 --prop axisMax=3500 --prop majorUnit=500 +# +# Features: axisMin, axisMax, majorUnit +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Waterfall Labels & Axis" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Custom Axis Range"' + f' --prop data=Base:2000,Up:800,Down:-500,Total:2300' + f' --prop increaseColor=2E75B6' + f' --prop decreaseColor=C00000' + f' --prop totalColor=FFC000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop axisMin=0 --prop axisMax=3500 --prop majorUnit=500') + +# -------------------------------------------------------------------------- +# Chart 3: Legend positioning and legendfont +# +# officecli add charts-waterfall.xlsx "/3-Waterfall Labels & Axis" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Legend Styling" \ +# --prop data="Begin:3000,Earned:1100,Spent:-700,End:3400" \ +# --prop increaseColor=70AD47 \ +# --prop decreaseColor=FF0000 \ +# --prop totalColor=4472C4 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop legend=right \ +# --prop legendfont=10:1F4E79:Helvetica +# +# Features: legend=right, legendfont (size:color:font) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Waterfall Labels & Axis" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Legend Styling"' + f' --prop data=Begin:3000,Earned:1100,Spent:-700,End:3400' + f' --prop increaseColor=70AD47' + f' --prop decreaseColor=FF0000' + f' --prop totalColor=4472C4' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop legend=right' + f' --prop legendfont=10:1F4E79:Helvetica') + +# -------------------------------------------------------------------------- +# Chart 4: Manual layout with plotArea.x/y/w/h +# +# officecli add charts-waterfall.xlsx "/3-Waterfall Labels & Axis" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Manual Plot Layout" \ +# --prop data="Start:1800,Add:600,Sub:-400,End:2000" \ +# --prop increaseColor=548235 \ +# --prop decreaseColor=BF0000 \ +# --prop totalColor=2F5496 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop plotArea.x=0.15 --prop plotArea.y=0.15 \ +# --prop plotArea.w=0.75 --prop plotArea.h=0.70 +# +# Features: plotArea.x/y/w/h (manual layout, fractional coordinates) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/3-Waterfall Labels & Axis" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Manual Plot Layout"' + f' --prop data=Start:1800,Add:600,Sub:-400,End:2000' + f' --prop increaseColor=548235' + f' --prop decreaseColor=BF0000' + f' --prop totalColor=2F5496' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop plotArea.x=0.15 --prop plotArea.y=0.15' + f' --prop plotArea.w=0.75 --prop plotArea.h=0.70') + +# ========================================================================== +# Sheet: 4-Waterfall Advanced +# ========================================================================== +print("\n--- 4-Waterfall Advanced ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Waterfall Advanced"') + +# -------------------------------------------------------------------------- +# Chart 1: Waterfall with referenceLine +# +# officecli add charts-waterfall.xlsx "/4-Waterfall Advanced" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Reference Line" \ +# --prop data="Start:2000,Revenue:900,Refunds:-300,Fees:-200,Net:2400" \ +# --prop increaseColor=70AD47 \ +# --prop decreaseColor=FF0000 \ +# --prop totalColor=4472C4 \ +# --prop x=0 --prop y=0 --prop width=12 --prop height=18 \ +# --prop referenceLine=2000:FF0000:Target:dash +# +# Features: referenceLine (value:label-color-dash-width) +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Waterfall Advanced" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Reference Line"' + f' --prop data=Start:2000,Revenue:900,Refunds:-300,Fees:-200,Net:2400' + f' --prop increaseColor=70AD47' + f' --prop decreaseColor=FF0000' + f' --prop totalColor=4472C4' + f' --prop x=0 --prop y=0 --prop width=12 --prop height=18' + f' --prop referenceLine=2000:FF0000:Target:dash') + +# -------------------------------------------------------------------------- +# Chart 2: Axis line and category axis line styling +# +# officecli add charts-waterfall.xlsx "/4-Waterfall Advanced" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Axis Line Styling" \ +# --prop data="Open:1500,Deposit:700,Withdraw:-400,Close:1800" \ +# --prop increaseColor=2E75B6 \ +# --prop decreaseColor=C00000 \ +# --prop totalColor=FFC000 \ +# --prop x=13 --prop y=0 --prop width=12 --prop height=18 \ +# --prop axisLine=333333:2 \ +# --prop catAxisLine=333333:2 +# +# Features: axisLine (color-width), catAxisLine +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Waterfall Advanced" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Axis Line Styling"' + f' --prop data=Open:1500,Deposit:700,Withdraw:-400,Close:1800' + f' --prop increaseColor=2E75B6' + f' --prop decreaseColor=C00000' + f' --prop totalColor=FFC000' + f' --prop x=13 --prop y=0 --prop width=12 --prop height=18' + f' --prop axisLine=333333:2' + f' --prop catAxisLine=333333:2') + +# -------------------------------------------------------------------------- +# Chart 3: Title glow and shadow effects +# +# officecli add charts-waterfall.xlsx "/4-Waterfall Advanced" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Glow & Shadow Effects" \ +# --prop data="Base:3000,Inflow:1200,Outflow:-800,Balance:3400" \ +# --prop increaseColor=70AD47 \ +# --prop decreaseColor=FF0000 \ +# --prop totalColor=4472C4 \ +# --prop x=0 --prop y=19 --prop width=12 --prop height=18 \ +# --prop title.glow=4472C4-8 \ +# --prop title.shadow=000000-3-315-2-30 \ +# --prop title.size=16 --prop title.bold=true +# +# Features: title.glow (color-radius), title.shadow +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Waterfall Advanced" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Glow & Shadow Effects"' + f' --prop data=Base:3000,Inflow:1200,Outflow:-800,Balance:3400' + f' --prop increaseColor=70AD47' + f' --prop decreaseColor=FF0000' + f' --prop totalColor=4472C4' + f' --prop x=0 --prop y=19 --prop width=12 --prop height=18' + f' --prop title.glow=4472C4-8' + f' --prop title.shadow=000000-3-315-2-30' + f' --prop title.size=16 --prop title.bold=true') + +# -------------------------------------------------------------------------- +# Chart 4: Large dataset waterfall (8+ categories) +# +# officecli add charts-waterfall.xlsx "/4-Waterfall Advanced" --type chart \ +# --prop chartType=waterfall \ +# --prop title="Annual P&L Detail" \ +# --prop data="Revenue:8500,COGS:-3400,Gross Profit:5100,R&D:-1200,Sales:-900,Marketing:-600,G&A:-500,EBITDA:1900,Depreciation:-300,Interest:-200,Tax:-350,Net Income:1050" \ +# --prop increaseColor=548235 \ +# --prop decreaseColor=C00000 \ +# --prop totalColor=2F5496 \ +# --prop x=13 --prop y=19 --prop width=12 --prop height=18 \ +# --prop dataLabels=true \ +# --prop axisfont=8:333333:Calibri +# +# Features: large dataset (12 categories), axisfont with smaller size +# for readability +# -------------------------------------------------------------------------- +cli(f'add "{FILE}" "/4-Waterfall Advanced" --type chart' + f' --prop chartType=waterfall' + f' --prop title="Annual P&L Detail"' + f' --prop "data=Revenue:8500,COGS:-3400,Gross Profit:5100,R&D:-1200,Sales:-900,Marketing:-600,G&A:-500,EBITDA:1900,Depreciation:-300,Interest:-200,Tax:-350,Net Income:1050"' + f' --prop increaseColor=548235' + f' --prop decreaseColor=C00000' + f' --prop totalColor=2F5496' + f' --prop x=13 --prop y=19 --prop width=12 --prop height=18' + f' --prop dataLabels=true' + f' --prop axisfont=8:333333:Calibri') + +# Remove blank default Sheet1 (all data is inline) +cli(f'remove "{FILE}" /Sheet1') + +print(f"\nDone! Generated: {FILE}") +print(" 4 sheets (16 charts total)") +print(" Sheet 1: Waterfall Fundamentals (4 charts)") +print(" Sheet 2: Waterfall Styling (4 charts)") +print(" Sheet 3: Waterfall Labels & Axis (4 charts)") +print(" Sheet 4: Waterfall Advanced (4 charts)") diff --git a/examples/excel/charts/charts-waterfall.xlsx b/examples/excel/charts/charts-waterfall.xlsx new file mode 100644 index 000000000..26a2288d5 Binary files /dev/null and b/examples/excel/charts/charts-waterfall.xlsx differ diff --git a/examples/excel/outputs/beautiful_charts.xlsx b/examples/excel/outputs/beautiful_charts.xlsx deleted file mode 100644 index 561bf0f1d..000000000 Binary files a/examples/excel/outputs/beautiful_charts.xlsx and /dev/null differ diff --git a/examples/excel/outputs/charts_demo.xlsx b/examples/excel/outputs/charts_demo.xlsx deleted file mode 100644 index 127228b29..000000000 Binary files a/examples/excel/outputs/charts_demo.xlsx and /dev/null differ diff --git a/examples/excel/outputs/sales_report.xlsx b/examples/excel/outputs/sales_report.xlsx deleted file mode 100644 index d7768ccb2..000000000 Binary files a/examples/excel/outputs/sales_report.xlsx and /dev/null differ diff --git a/examples/excel/pivot-tables.md b/examples/excel/pivot-tables.md new file mode 100644 index 000000000..4b8f9c96e --- /dev/null +++ b/examples/excel/pivot-tables.md @@ -0,0 +1,366 @@ +# Pivot Table Showcase + +This demo consists of three files that work together: + +- **pivot-tables.py** — Python script that calls `officecli` commands to generate the workbook. Each pivot table command is shown as a copyable shell command in the comments, then executed by the script. Read this to learn the exact `officecli add --type pivottable --prop ...` syntax. +- **pivot-tables.xlsx** — The generated workbook with 19 sheets (Sheet1 + CNData + 17 pivot tables). Open in Excel to see the rendered pivot tables. Use `officecli get` or `officecli query` to inspect programmatically. +- **pivot-tables.md** — This file. Maps each sheet in the xlsx to the feature it demonstrates and the command that created it. + +## Regenerate + +```bash +cd examples/excel +python3 pivot-tables.py +# → pivot-tables.xlsx +``` + +## Source Data + +| Sheet | Rows | Columns | Purpose | +|-------|------|---------|---------| +| Sheet1 | 50 | Region, Category, Product, Quarter, Sales, Quantity, Cost, Channel, Priority, Date | English sales data spanning 2024-2025 | +| CNData | 12 | 地区, 品类, 销售额 | Chinese sales data for locale sort demo | + +## Pivot Tables + +### Sheet: 1-Sales Overview + +The most feature-rich pivot. Tabular layout with 2-level row hierarchy crossed against quarterly columns. Three value fields where Cost is shown as percentage of row total. Dual page filters let users slice by Channel and Priority. Outer row labels repeat on every row. + +```bash +officecli add pivot-tables.xlsx "/1-Sales Overview" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Region,Category \ + --prop cols=Quarter \ + --prop 'values=Sales:sum,Quantity:sum,Cost:sum:percent_of_row' \ + --prop 'filters=Channel,Priority' \ + --prop layout=tabular \ + --prop repeatlabels=true \ + --prop grandtotals=both \ + --prop subtotals=on \ + --prop sort=desc \ + --prop style=PivotStyleDark2 +``` + +**Features:** `layout=tabular`, `repeatlabels=true`, dual `filters`, `values` with `percent_of_row`, `sort=desc` + +### Sheet: 2-Market Share + +Each region's share within each category, shown as column percentages. Outline layout provides expand/collapse grouping. + +```bash +officecli add pivot-tables.xlsx "/2-Market Share" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Region \ + --prop cols=Category \ + --prop 'values=Sales:sum:percent_of_col' \ + --prop filters=Channel \ + --prop layout=outline \ + --prop grandtotals=both \ + --prop style=PivotStyleMedium4 +``` + +**Features:** `layout=outline`, `values` with `percent_of_col` + +### Sheet: 3-Product Deep Dive + +Five value fields with three different aggregation functions on the same source column (Sales:sum, Sales:average, Sales:max). No column axis — values become column headers automatically. + +```bash +officecli add pivot-tables.xlsx "/3-Product Deep Dive" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Category,Product \ + --prop 'values=Sales:sum,Sales:average,Sales:max,Quantity:sum,Cost:sum' \ + --prop filters=Region \ + --prop layout=tabular \ + --prop grandtotals=rows \ + --prop subtotals=on \ + --prop sort=desc \ + --prop style=PivotStyleMedium9 +``` + +**Features:** 5 `values` fields, no `cols` (synthetic Values axis), `grandtotals=rows` + +### Sheet: 4-Channel Analysis + +Sales shown as percentage of the grand total — reveals each channel's global share across quarters. No page filters. + +```bash +officecli add pivot-tables.xlsx "/4-Channel Analysis" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Channel \ + --prop cols=Quarter \ + --prop 'values=Sales:sum:percent_of_total,Quantity:sum' \ + --prop layout=outline \ + --prop grandtotals=both \ + --prop style=PivotStyleLight21 +``` + +**Features:** `values` with `percent_of_total`, no `filters` + +### Sheet: 5-Priority Matrix + +Blank rows inserted after each outer group (Priority) for visual separation. Ascending sort puts High first. + +```bash +officecli add pivot-tables.xlsx "/5-Priority Matrix" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Priority,Region \ + --prop cols=Category \ + --prop 'values=Sales:sum,Cost:sum:percent_of_row' \ + --prop filters=Channel \ + --prop layout=tabular \ + --prop blankrows=true \ + --prop grandtotals=both \ + --prop subtotals=on \ + --prop sort=asc \ + --prop style=PivotStyleDark6 +``` + +**Features:** `blankrows=true`, `sort=asc` + +### Sheet: 6-Compact 3-Level + +Three-level row hierarchy (Region > Category > Product) in compact layout — all labels share one column with progressive indentation. + +```bash +officecli add pivot-tables.xlsx "/6-Compact 3-Level" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Region,Category,Product \ + --prop 'values=Sales:sum,Quantity:sum' \ + --prop filters=Priority \ + --prop layout=compact \ + --prop grandtotals=both \ + --prop subtotals=on \ + --prop sort=desc \ + --prop style=PivotStyleMedium14 +``` + +**Features:** `layout=compact`, 3-level `rows` + +### Sheet: 7-No Subtotals + +Flat tabular view with subtotals disabled. Only the bottom grand total row remains. Outer labels are repeated on every row since there are no subtotal rows to carry them. + +```bash +officecli add pivot-tables.xlsx "/7-No Subtotals" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Region,Category \ + --prop cols=Quarter \ + --prop values=Sales:sum \ + --prop layout=tabular \ + --prop repeatlabels=true \ + --prop grandtotals=cols \ + --prop subtotals=off \ + --prop sort=asc \ + --prop style=PivotStyleLight1 +``` + +**Features:** `subtotals=off`, `grandtotals=cols`, `repeatlabels=true` + +### Sheet: 8-Date Grouping + +Automatic date grouping from a date column. `Date:year` creates year buckets ("2024", "2025"), `Date:quarter` creates quarter sub-buckets ("2024-Q1", ...). Uses native Excel fieldGroup XML. + +```bash +officecli add pivot-tables.xlsx "/8-Date Grouping" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop 'rows=Date:year,Date:quarter' \ + --prop 'values=Sales:sum,Cost:sum' \ + --prop filters=Region \ + --prop layout=outline \ + --prop grandtotals=both \ + --prop subtotals=on \ + --prop style=PivotStyleMedium7 +``` + +**Features:** `rows` with `Date:year,Date:quarter` date grouping syntax + +### Sheet: 9-Top 5 Products + +Only the top 5 products by sales are shown. Grand totals are hidden entirely. + +```bash +officecli add pivot-tables.xlsx "/9-Top 5 Products" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Product \ + --prop 'values=Sales:sum,Quantity:sum,Cost:sum' \ + --prop layout=tabular \ + --prop grandtotals=none \ + --prop topN=5 \ + --prop sort=desc \ + --prop style=PivotStyleDark1 +``` + +**Features:** `topN=5`, `grandtotals=none` + +### Sheet: 10-Ultimate + +Every feature combined in one pivot table — the kitchen sink. + +```bash +officecli add pivot-tables.xlsx "/10-Ultimate" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Region,Category \ + --prop cols=Quarter \ + --prop 'values=Sales:sum,Quantity:average,Cost:sum:percent_of_row' \ + --prop 'filters=Channel,Priority' \ + --prop layout=tabular \ + --prop repeatlabels=true \ + --prop blankrows=true \ + --prop grandtotals=rows \ + --prop subtotals=on \ + --prop sort=desc \ + --prop style=PivotStyleDark11 +``` + +**Features:** `repeatlabels=true` + `blankrows=true` + dual `filters` + mixed aggregations + `grandtotals=rows` + +### Sheet: 11-Chinese Locale + +Chinese data with pinyin-order sorting and a custom grand total label. Demonstrates that field names, filter values, and captions all work with non-ASCII text. + +```bash +officecli add pivot-tables.xlsx "/11-Chinese Locale" --type pivottable \ + --prop source=CNData!A1:C13 \ + --prop rows=地区,品类 \ + --prop values=销售额:sum \ + --prop layout=tabular \ + --prop grandtotals=both \ + --prop subtotals=on \ + --prop sort=locale \ + --prop grandTotalCaption=合计 \ + --prop style=PivotStyleMedium2 +``` + +**Features:** `sort=locale` (pinyin: 华北 < 华东 < 华南 < 西南), `grandTotalCaption` + +### Sheet: 12-Position + Aggregates + +Anchors the pivot at cell D2 instead of the auto-placed default. Demonstrates the less-common value aggregations (count, min, product, countNums) and the `aggregate=avg` default used when a value tuple omits its aggregation. + +```bash +officecli add pivot-tables.xlsx "/12-Position + Aggregates" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop position=D2 \ + --prop rows=Category \ + --prop 'values=Sales:count,Quantity:min,Quantity:product,Sales:countNums' \ + --prop aggregate=avg \ + --prop layout=tabular \ + --prop grandtotals=both \ + --prop style=PivotStyleLight16 +``` + +**Features:** `position=D2`, `aggregate=avg` (default agg), value aggs `count` / `min` / `product` / `countNums` + +### Sheet: 13-Calculated Field + +User-defined formula fields (`Margin = Sales - Cost`, `Tax = Sales * 0.1`) are auto-added as data fields — no need to list them in `values=`. A pre-cache `labelFilter` keeps only rows where Region begins with "N". + +```bash +officecli add pivot-tables.xlsx "/13-Calculated Field" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop 'calculatedField1=Margin:=Sales-Cost' \ + --prop 'calculatedField2=Tax:=Sales*0.1' \ + --prop rows=Region \ + --prop values=Sales:sum \ + --prop 'labelFilter=Region:beginsWith:N' \ + --prop layout=tabular \ + --prop grandtotals=both \ + --prop style=PivotStyleMedium3 +``` + +**Features:** `calculatedField1` / `calculatedField2`, `labelFilter` + +### Sheet: 14-Statistical + +Completes the aggregate set with sample/population variance (`var` / `varP`). `showDataAs=running_total` is set as a standalone prop (vs the per-value `Field:agg:mode` tuple) and applies to all value fields as the default display. + +```bash +officecli add pivot-tables.xlsx "/14-Statistical" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Region \ + --prop cols=Quarter \ + --prop 'values=Sales:var,Sales:varP,Sales:sum' \ + --prop showDataAs=running_total \ + --prop layout=tabular \ + --prop grandtotals=both \ + --prop style=PivotStyleLight10 +``` + +**Features:** `Sales:var`, `Sales:varP`, `showDataAs=running_total` (standalone) + +### Sheet: 15-Independent Totals + +Row and column grand totals toggled independently (vs the combined `grandtotals=both/rows/cols/none`). `defaultSubtotal=true` sets the default-subtotal flag on every pivotField. `sort=locale-desc` reverses pinyin order. + +```bash +officecli add pivot-tables.xlsx "/15-Independent Totals" --type pivottable \ + --prop source=CNData!A1:C13 \ + --prop rows=地区 \ + --prop cols=品类 \ + --prop values=销售额:sum \ + --prop rowGrandTotals=true \ + --prop colGrandTotals=false \ + --prop defaultSubtotal=true \ + --prop layout=outline \ + --prop subtotals=on \ + --prop sort=locale-desc \ + --prop style=PivotStyleMedium11 +``` + +**Features:** `rowGrandTotals` / `colGrandTotals` (independent), `defaultSubtotal`, `sort=locale-desc` + +### Sheet: 16-Style Flags + +All five `pivotTableStyleInfo` flags wired up — row/col banding, row/col header emphasis, last-column highlight. These map directly to the checkboxes in Excel's PivotTable Styles ribbon. + +```bash +officecli add pivot-tables.xlsx "/16-Style Flags" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Region,Category \ + --prop cols=Quarter \ + --prop values=Sales:sum \ + --prop showRowStripes=true \ + --prop showColStripes=true \ + --prop showRowHeaders=true \ + --prop showColHeaders=true \ + --prop showLastColumn=true \ + --prop layout=tabular \ + --prop grandtotals=both \ + --prop style=PivotStyleMedium17 +``` + +**Features:** `showRowStripes`, `showColStripes`, `showRowHeaders`, `showColHeaders`, `showLastColumn` + +### Sheet: 17-Display Toggles + +`showDrill=false` hides the +/- expand-collapse buttons on every field. `mergeLabels=true` merges and centers repeated outer-axis item cells (``). + +```bash +officecli add pivot-tables.xlsx "/17-Display Toggles" --type pivottable \ + --prop source=Sheet1!A1:J51 \ + --prop rows=Region,Category \ + --prop values=Sales:sum \ + --prop showDrill=false \ + --prop mergeLabels=true \ + --prop layout=outline \ + --prop grandtotals=both \ + --prop subtotals=on \ + --prop style=PivotStyleLight19 +``` + +**Features:** `showDrill=false`, `mergeLabels=true` + +## Inspect the Generated File + +```bash +# List all pivot tables +officecli query pivot-tables.xlsx pivottable + +# Get details of a specific pivot +officecli get pivot-tables.xlsx "/1-Sales Overview/pivottable[1]" + +# View rendered data as text +officecli view pivot-tables.xlsx text --sheet "1-Sales Overview" +``` diff --git a/examples/excel/pivot-tables.py b/examples/excel/pivot-tables.py new file mode 100644 index 000000000..4e10c419a --- /dev/null +++ b/examples/excel/pivot-tables.py @@ -0,0 +1,682 @@ +#!/usr/bin/env python3 +""" +Pivot Table Showcase — generates pivot-tables.xlsx with 17 pivot tables. + +Each pivot table demonstrates different officecli features. +See pivot-tables.md for a guide to each sheet in the generated file. + +Usage: + python3 pivot-tables.py +""" + +import subprocess, sys, os, json, atexit + +FILE = "pivot-tables.xlsx" + +def cli(cmd): + """Run: officecli """ + r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True) + out = (r.stdout or "").strip() + if out: + for line in out.split("\n"): + if line.strip(): + print(f" {line.strip()}") + if r.returncode != 0: + err = (r.stderr or "").strip() + if err and "UNSUPPORTED" not in err and "process cannot access" not in err: + print(f" ERROR: {err}") + +if os.path.exists(FILE): + os.remove(FILE) + +cli(f'create "{FILE}"') +cli(f'open "{FILE}"') +atexit.register(lambda: cli(f'close "{FILE}"')) + +# ========================================================================== +# Source data — batch is used here only for speed (500+ cell writes). +# ========================================================================== +print("\n--- Populating source data ---") + +data_cmds = [] +for j, h in enumerate(["Region","Category","Product","Quarter","Sales","Quantity","Cost","Channel","Priority","Date"]): + data_cmds.append({"command":"set","path":f"/Sheet1/{'ABCDEFGHIJ'[j]}1","props":{"text":h}}) + +rows = [ + ("North","Electronics","Laptop","Q1",12500,45,7500,"Online","High","2025-01-15"), + ("North","Electronics","Phone","Q1",8900,120,5340,"Retail","High","2025-02-10"), + ("North","Electronics","Tablet","Q2",6200,38,3720,"Online","Medium","2025-04-20"), + ("North","Electronics","Laptop","Q2",15800,55,9480,"Retail","High","2025-05-08"), + ("North","Electronics","Phone","Q3",11200,150,6720,"Online","High","2025-07-12"), + ("North","Electronics","Tablet","Q4",9500,62,5700,"Retail","Medium","2025-10-05"), + ("North","Clothing","Jacket","Q1",4200,85,2100,"Retail","Low","2025-01-22"), + ("North","Clothing","Shoes","Q2",5600,70,2800,"Online","Medium","2025-04-15"), + ("North","Clothing","Hat","Q3",1800,110,900,"Retail","Low","2025-08-03"), + ("North","Clothing","Jacket","Q4",7800,95,3900,"Online","High","2025-11-18"), + ("North","Food","Coffee","Q1",2400,200,1200,"Retail","Low","2025-03-01"), + ("North","Food","Snacks","Q2",1500,180,750,"Online","Low","2025-06-10"), + ("North","Food","Juice","Q3",1900,160,950,"Retail","Medium","2025-09-20"), + ("North","Food","Coffee","Q4",3200,220,1600,"Online","Medium","2025-12-01"), + ("South","Electronics","Phone","Q1",18500,200,11100,"Online","High","2024-01-20"), + ("South","Electronics","Laptop","Q2",22000,72,13200,"Retail","High","2024-05-15"), + ("South","Electronics","Tablet","Q3",7800,48,4680,"Online","Medium","2024-08-22"), + ("South","Electronics","Phone","Q4",14200,165,8520,"Retail","High","2024-11-30"), + ("South","Clothing","Shoes","Q1",9200,110,4600,"Retail","Medium","2024-02-14"), + ("South","Clothing","Jacket","Q2",6500,78,3250,"Online","Low","2024-06-01"), + ("South","Clothing","Hat","Q3",3100,130,1550,"Retail","Low","2024-09-10"), + ("South","Clothing","Shoes","Q4",8800,98,4400,"Online","Medium","2024-12-20"), + ("South","Food","Juice","Q1",1800,240,900,"Retail","Low","2024-03-08"), + ("South","Food","Coffee","Q2",3500,280,1750,"Online","Medium","2024-04-25"), + ("South","Food","Snacks","Q3",2200,190,1100,"Retail","Low","2024-07-14"), + ("South","Food","Juice","Q4",2800,210,1400,"Online","Medium","2024-10-18"), + ("East","Electronics","Tablet","Q1",5400,35,3240,"Online","Medium","2025-02-28"), + ("East","Electronics","Laptop","Q2",19500,65,11700,"Retail","High","2025-05-20"), + ("East","Electronics","Phone","Q3",13800,180,8280,"Online","High","2025-08-15"), + ("East","Electronics","Tablet","Q4",8200,52,4920,"Retail","Medium","2025-11-02"), + ("East","Clothing","Hat","Q1",2800,140,1400,"Retail","Low","2025-01-05"), + ("East","Clothing","Jacket","Q2",7200,60,3600,"Online","Medium","2025-06-18"), + ("East","Clothing","Shoes","Q3",5500,88,2750,"Retail","Medium","2025-09-25"), + ("East","Clothing","Hat","Q4",3600,105,1800,"Online","Low","2025-12-10"), + ("East","Food","Snacks","Q1",1200,300,600,"Retail","Low","2025-03-15"), + ("East","Food","Juice","Q2",2100,170,1050,"Online","Medium","2025-04-30"), + ("East","Food","Coffee","Q3",2800,230,1400,"Retail","Medium","2025-07-22"), + ("East","Food","Snacks","Q4",1600,250,800,"Online","Low","2025-10-28"), + ("West","Electronics","Laptop","Q1",20500,68,12300,"Online","High","2024-01-10"), + ("West","Electronics","Phone","Q2",16800,190,10080,"Retail","High","2024-04-05"), + ("West","Electronics","Tablet","Q3",8900,55,5340,"Online","Medium","2024-08-12"), + ("West","Electronics","Laptop","Q4",25000,82,15000,"Retail","High","2024-11-15"), + ("West","Clothing","Jacket","Q1",11000,88,5500,"Retail","Medium","2024-02-22"), + ("West","Clothing","Shoes","Q2",7500,95,3750,"Online","Medium","2024-05-30"), + ("West","Clothing","Hat","Q3",4200,120,2100,"Retail","Low","2024-09-08"), + ("West","Clothing","Jacket","Q4",13500,105,6750,"Online","High","2024-12-01"), + ("West","Food","Coffee","Q1",4500,350,2250,"Online","Medium","2024-03-18"), + ("West","Food","Snacks","Q2",2800,280,1400,"Online","Medium","2024-06-22"), + ("West","Food","Juice","Q3",3200,260,1600,"Retail","Low","2024-07-30"), + ("West","Food","Coffee","Q4",5800,400,2900,"Online","High","2024-10-25"), +] +C = "ABCDEFGHIJ" +for i, row in enumerate(rows): + for j, val in enumerate(row): + data_cmds.append({"command":"set","path":f"/Sheet1/{C[j]}{i+2}","props":{"text":str(val)}}) + +data_cmds.append({"command":"add","parent":"/","type":"sheet","props":{"name":"CNData"}}) +for j, h in enumerate(["地区","品类","销售额"]): + data_cmds.append({"command":"set","path":f"/CNData/{C[j]}1","props":{"text":h}}) +for i, (r, c, s) in enumerate([ + ("华东","电子产品",18000),("华东","服装",9500),("华东","食品",4200), + ("华南","电子产品",22000),("华南","服装",12000),("华南","食品",5800), + ("华北","电子产品",15000),("华北","服装",7800),("华北","食品",3600), + ("西南","电子产品",11000),("西南","服装",6500),("西南","食品",2900), +]): + for j, val in enumerate([r, c, s]): + data_cmds.append({"command":"set","path":f"/CNData/{C[j]}{i+2}","props":{"text":str(val)}}) + +cli(f'batch "{FILE}" --force --commands \'{json.dumps(data_cmds)}\'') + +# ========================================================================== +# 17 Pivot Tables +# +# Each section below shows the exact officecli command in a comment block, +# then executes it. You can copy any command block and run it in a terminal. +# ========================================================================== + +# -------------------------------------------------------------------------- +# Sheet: 1-Sales Overview +# +# officecli add pivot-tables.xlsx "/1-Sales Overview" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Region,Category \ +# --prop cols=Quarter \ +# --prop 'values=Sales:sum,Quantity:sum,Cost:sum:percent_of_row' \ +# --prop 'filters=Channel,Priority' \ +# --prop layout=tabular \ +# --prop repeatlabels=true \ +# --prop grandtotals=both \ +# --prop subtotals=on \ +# --prop sort=desc \ +# --prop name=SalesOverview \ +# --prop style=PivotStyleDark2 +# +# Features: tabular layout, 2-level rows, column axis, 3 value fields, +# Cost as percent_of_row, dual page filters, repeat item labels, desc sort +# -------------------------------------------------------------------------- +print("\n--- 1-Sales Overview ---") +cli(f'add "{FILE}" / --type sheet --prop name="1-Sales Overview"') +cli(f'add "{FILE}" "/1-Sales Overview" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Region,Category' + f' --prop cols=Quarter' + f' --prop values=Sales:sum,Quantity:sum,Cost:sum:percent_of_row' + f' --prop filters=Channel,Priority' + f' --prop layout=tabular' + f' --prop repeatlabels=true' + f' --prop grandtotals=both' + f' --prop subtotals=on' + f' --prop sort=desc' + f' --prop name=SalesOverview' + f' --prop style=PivotStyleDark2') + +# -------------------------------------------------------------------------- +# Sheet: 2-Market Share +# +# officecli add pivot-tables.xlsx "/2-Market Share" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Region \ +# --prop cols=Category \ +# --prop 'values=Sales:sum:percent_of_col' \ +# --prop filters=Channel \ +# --prop layout=outline \ +# --prop grandtotals=both \ +# --prop name=MarketShare \ +# --prop style=PivotStyleMedium4 +# +# Features: outline layout, percent_of_col (each region's share per category) +# -------------------------------------------------------------------------- +print("\n--- 2-Market Share ---") +cli(f'add "{FILE}" / --type sheet --prop name="2-Market Share"') +cli(f'add "{FILE}" "/2-Market Share" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Region' + f' --prop cols=Category' + f' --prop values=Sales:sum:percent_of_col' + f' --prop filters=Channel' + f' --prop layout=outline' + f' --prop grandtotals=both' + f' --prop name=MarketShare' + f' --prop style=PivotStyleMedium4') + +# -------------------------------------------------------------------------- +# Sheet: 3-Product Deep Dive +# +# officecli add pivot-tables.xlsx "/3-Product Deep Dive" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Category,Product \ +# --prop 'values=Sales:sum,Sales:average,Sales:max,Quantity:sum,Cost:sum' \ +# --prop filters=Region \ +# --prop layout=tabular \ +# --prop grandtotals=rows \ +# --prop subtotals=on \ +# --prop sort=desc \ +# --prop name=ProductDeepDive \ +# --prop style=PivotStyleMedium9 +# +# Features: 5 value fields (sum, average, max), no column axis — values +# become column headers via synthetic "Values" axis, row grand totals only +# -------------------------------------------------------------------------- +print("\n--- 3-Product Deep Dive ---") +cli(f'add "{FILE}" / --type sheet --prop name="3-Product Deep Dive"') +cli(f'add "{FILE}" "/3-Product Deep Dive" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Category,Product' + f' --prop values=Sales:sum,Sales:average,Sales:max,Quantity:sum,Cost:sum' + f' --prop filters=Region' + f' --prop layout=tabular' + f' --prop grandtotals=rows' + f' --prop subtotals=on' + f' --prop sort=desc' + f' --prop name=ProductDeepDive' + f' --prop style=PivotStyleMedium9') + +# -------------------------------------------------------------------------- +# Sheet: 4-Channel Analysis +# +# officecli add pivot-tables.xlsx "/4-Channel Analysis" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Channel \ +# --prop cols=Quarter \ +# --prop 'values=Sales:sum:percent_of_total,Quantity:sum' \ +# --prop layout=outline \ +# --prop grandtotals=both \ +# --prop name=ChannelTrend \ +# --prop style=PivotStyleLight21 +# +# Features: percent_of_total (global share), no filters +# -------------------------------------------------------------------------- +print("\n--- 4-Channel Analysis ---") +cli(f'add "{FILE}" / --type sheet --prop name="4-Channel Analysis"') +cli(f'add "{FILE}" "/4-Channel Analysis" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Channel' + f' --prop cols=Quarter' + f' --prop values=Sales:sum:percent_of_total,Quantity:sum' + f' --prop layout=outline' + f' --prop grandtotals=both' + f' --prop name=ChannelTrend' + f' --prop style=PivotStyleLight21') + +# -------------------------------------------------------------------------- +# Sheet: 5-Priority Matrix +# +# officecli add pivot-tables.xlsx "/5-Priority Matrix" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Priority,Region \ +# --prop cols=Category \ +# --prop 'values=Sales:sum,Cost:sum:percent_of_row' \ +# --prop filters=Channel \ +# --prop layout=tabular \ +# --prop blankrows=true \ +# --prop grandtotals=both \ +# --prop subtotals=on \ +# --prop sort=asc \ +# --prop name=PriorityMatrix \ +# --prop style=PivotStyleDark6 +# +# Features: blankRows — empty line after each outer group for visual separation +# -------------------------------------------------------------------------- +print("\n--- 5-Priority Matrix ---") +cli(f'add "{FILE}" / --type sheet --prop name="5-Priority Matrix"') +cli(f'add "{FILE}" "/5-Priority Matrix" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Priority,Region' + f' --prop cols=Category' + f' --prop values=Sales:sum,Cost:sum:percent_of_row' + f' --prop filters=Channel' + f' --prop layout=tabular' + f' --prop blankrows=true' + f' --prop grandtotals=both' + f' --prop subtotals=on' + f' --prop sort=asc' + f' --prop name=PriorityMatrix' + f' --prop style=PivotStyleDark6') + +# -------------------------------------------------------------------------- +# Sheet: 6-Compact 3-Level +# +# officecli add pivot-tables.xlsx "/6-Compact 3-Level" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Region,Category,Product \ +# --prop 'values=Sales:sum,Quantity:sum' \ +# --prop filters=Priority \ +# --prop layout=compact \ +# --prop grandtotals=both \ +# --prop subtotals=on \ +# --prop sort=desc \ +# --prop name=Compact3Level \ +# --prop style=PivotStyleMedium14 +# +# Features: compact layout — 3-level hierarchy in one indented column +# -------------------------------------------------------------------------- +print("\n--- 6-Compact 3-Level ---") +cli(f'add "{FILE}" / --type sheet --prop name="6-Compact 3-Level"') +cli(f'add "{FILE}" "/6-Compact 3-Level" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Region,Category,Product' + f' --prop values=Sales:sum,Quantity:sum' + f' --prop filters=Priority' + f' --prop layout=compact' + f' --prop grandtotals=both' + f' --prop subtotals=on' + f' --prop sort=desc' + f' --prop name=Compact3Level' + f' --prop style=PivotStyleMedium14') + +# -------------------------------------------------------------------------- +# Sheet: 7-No Subtotals +# +# officecli add pivot-tables.xlsx "/7-No Subtotals" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Region,Category \ +# --prop cols=Quarter \ +# --prop values=Sales:sum \ +# --prop layout=tabular \ +# --prop repeatlabels=true \ +# --prop grandtotals=cols \ +# --prop subtotals=off \ +# --prop sort=asc \ +# --prop name=FlatView \ +# --prop style=PivotStyleLight1 +# +# Features: subtotals=off (flat view), grandtotals=cols (bottom row only), +# repeatlabels=true (essential when subtotals off — otherwise outer labels vanish) +# -------------------------------------------------------------------------- +print("\n--- 7-No Subtotals ---") +cli(f'add "{FILE}" / --type sheet --prop name="7-No Subtotals"') +cli(f'add "{FILE}" "/7-No Subtotals" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Region,Category' + f' --prop cols=Quarter' + f' --prop values=Sales:sum' + f' --prop layout=tabular' + f' --prop repeatlabels=true' + f' --prop grandtotals=cols' + f' --prop subtotals=off' + f' --prop sort=asc' + f' --prop name=FlatView' + f' --prop style=PivotStyleLight1') + +# -------------------------------------------------------------------------- +# Sheet: 8-Date Grouping +# +# officecli add pivot-tables.xlsx "/8-Date Grouping" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop 'rows=Date:year,Date:quarter' \ +# --prop 'values=Sales:sum,Cost:sum' \ +# --prop filters=Region \ +# --prop layout=outline \ +# --prop grandtotals=both \ +# --prop subtotals=on \ +# --prop name=DateGrouping \ +# --prop style=PivotStyleMedium7 +# +# Features: automatic date grouping — Date:year creates "2024","2025" buckets, +# Date:quarter creates "2024-Q1",... sub-buckets. Uses native Excel fieldGroup XML. +# -------------------------------------------------------------------------- +print("\n--- 8-Date Grouping ---") +cli(f'add "{FILE}" / --type sheet --prop name="8-Date Grouping"') +cli(f'add "{FILE}" "/8-Date Grouping" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Date:year,Date:quarter' + f' --prop values=Sales:sum,Cost:sum' + f' --prop filters=Region' + f' --prop layout=outline' + f' --prop grandtotals=both' + f' --prop subtotals=on' + f' --prop name=DateGrouping' + f' --prop style=PivotStyleMedium7') + +# -------------------------------------------------------------------------- +# Sheet: 9-Top 5 Products +# +# officecli add pivot-tables.xlsx "/9-Top 5 Products" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Product \ +# --prop 'values=Sales:sum,Quantity:sum,Cost:sum' \ +# --prop layout=tabular \ +# --prop grandtotals=none \ +# --prop topN=5 \ +# --prop sort=desc \ +# --prop name=Top5Products \ +# --prop style=PivotStyleDark1 +# +# Features: topN=5 (only top 5 products by first value field), grandtotals=none +# -------------------------------------------------------------------------- +print("\n--- 9-Top 5 Products ---") +cli(f'add "{FILE}" / --type sheet --prop name="9-Top 5 Products"') +cli(f'add "{FILE}" "/9-Top 5 Products" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Product' + f' --prop values=Sales:sum,Quantity:sum,Cost:sum' + f' --prop layout=tabular' + f' --prop grandtotals=none' + f' --prop topN=5' + f' --prop sort=desc' + f' --prop name=Top5Products' + f' --prop style=PivotStyleDark1') + +# -------------------------------------------------------------------------- +# Sheet: 10-Ultimate +# +# officecli add pivot-tables.xlsx "/10-Ultimate" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Region,Category \ +# --prop cols=Quarter \ +# --prop 'values=Sales:sum,Quantity:average,Cost:sum:percent_of_row' \ +# --prop 'filters=Channel,Priority' \ +# --prop layout=tabular \ +# --prop repeatlabels=true \ +# --prop blankrows=true \ +# --prop grandtotals=rows \ +# --prop subtotals=on \ +# --prop sort=desc \ +# --prop name=UltimatePivot \ +# --prop style=PivotStyleDark11 +# +# Features: ALL features combined — tabular + repeatLabels + blankRows + +# dual filters + 3 mixed-aggregation values + row-only grand totals +# -------------------------------------------------------------------------- +print("\n--- 10-Ultimate ---") +cli(f'add "{FILE}" / --type sheet --prop name="10-Ultimate"') +cli(f'add "{FILE}" "/10-Ultimate" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Region,Category' + f' --prop cols=Quarter' + f' --prop values=Sales:sum,Quantity:average,Cost:sum:percent_of_row' + f' --prop filters=Channel,Priority' + f' --prop layout=tabular' + f' --prop repeatlabels=true' + f' --prop blankrows=true' + f' --prop grandtotals=rows' + f' --prop subtotals=on' + f' --prop sort=desc' + f' --prop name=UltimatePivot' + f' --prop style=PivotStyleDark11') + +# -------------------------------------------------------------------------- +# Sheet: 11-Chinese Locale +# +# officecli add pivot-tables.xlsx "/11-Chinese Locale" --type pivottable \ +# --prop source=CNData!A1:C13 \ +# --prop rows=地区,品类 \ +# --prop values=销售额:sum \ +# --prop layout=tabular \ +# --prop grandtotals=both \ +# --prop subtotals=on \ +# --prop sort=locale \ +# --prop grandTotalCaption=合计 \ +# --prop name=ChineseLocale \ +# --prop style=PivotStyleMedium2 +# +# Features: sort=locale (Chinese pinyin: 华北 < 华东 < 华南 < 西南), +# grandTotalCaption=合计 (custom grand total label) +# -------------------------------------------------------------------------- +print("\n--- 11-Chinese Locale ---") +cli(f'add "{FILE}" / --type sheet --prop name="11-Chinese Locale"') +cli(f'add "{FILE}" "/11-Chinese Locale" --type pivottable' + f' --prop source=CNData!A1:C13' + f' --prop rows=地区,品类' + f' --prop values=销售额:sum' + f' --prop layout=tabular' + f' --prop grandtotals=both' + f' --prop subtotals=on' + f' --prop sort=locale' + f' --prop grandTotalCaption=合计' + f' --prop name=ChineseLocale' + f' --prop style=PivotStyleMedium2') + +# -------------------------------------------------------------------------- +# Sheet: 12-Position + Aggregates +# +# officecli add pivot-tables.xlsx "/12-Position + Aggregates" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop position=D2 \ +# --prop rows=Category \ +# --prop 'values=Sales:count,Quantity:min,Quantity:product,Sales:countNums' \ +# --prop aggregate=avg \ +# --prop layout=tabular \ +# --prop grandtotals=both \ +# --prop name=PositionAggs \ +# --prop style=PivotStyleLight16 +# +# Features: position=D2 (anchor cell override, default is auto-place after source), +# aggregate=avg (default agg when omitted from a value tuple), +# value aggregations: count, min, product, countNums (sum/avg/max shown elsewhere) +# -------------------------------------------------------------------------- +print("\n--- 12-Position + Aggregates ---") +cli(f'add "{FILE}" / --type sheet --prop name="12-Position + Aggregates"') +cli(f'add "{FILE}" "/12-Position + Aggregates" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop position=D2' + f' --prop rows=Category' + f' --prop values=Sales:count,Quantity:min,Quantity:product,Sales:countNums' + f' --prop aggregate=avg' + f' --prop layout=tabular' + f' --prop grandtotals=both' + f' --prop name=PositionAggs' + f' --prop style=PivotStyleLight16') + +# -------------------------------------------------------------------------- +# Sheet: 13-Calculated Field +# +# officecli add pivot-tables.xlsx "/13-Calculated Field" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop 'calculatedField1=Margin:=Sales-Cost' \ +# --prop 'calculatedField2=Tax:=Sales*0.1' \ +# --prop rows=Region \ +# --prop values=Sales:sum \ +# --prop 'labelFilter=Region:beginsWith:N' \ +# --prop layout=tabular \ +# --prop grandtotals=both \ +# --prop name=CalcField \ +# --prop style=PivotStyleMedium3 +# +# Features: calculatedField1/2 — user-defined formula fields auto-added as +# data fields (no need to mention in values=). labelFilter — pre-cache row +# filter ('Region:beginsWith:N' keeps only Region values starting with N). +# -------------------------------------------------------------------------- +print("\n--- 13-Calculated Field ---") +cli(f'add "{FILE}" / --type sheet --prop name="13-Calculated Field"') +cli(f'add "{FILE}" "/13-Calculated Field" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop calculatedField1=Margin:=Sales-Cost' + f' --prop calculatedField2=Tax:=Sales*0.1' + f' --prop rows=Region' + f' --prop values=Sales:sum' + f' --prop labelFilter=Region:beginsWith:N' + f' --prop layout=tabular' + f' --prop grandtotals=both' + f' --prop name=CalcField' + f' --prop style=PivotStyleMedium3') + +# -------------------------------------------------------------------------- +# Sheet: 14-Statistical +# +# officecli add pivot-tables.xlsx "/14-Statistical" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Region \ +# --prop cols=Quarter \ +# --prop 'values=Sales:var,Sales:varP,Sales:sum' \ +# --prop showDataAs=running_total \ +# --prop layout=tabular \ +# --prop grandtotals=both \ +# --prop name=Statistical \ +# --prop style=PivotStyleLight10 +# +# Features: var / varP (sample + population variance) — completes the aggregate +# set. showDataAs=running_total as a standalone --prop (vs the tuple form +# 'Field:agg:mode'); applies as default display for all value fields. +# -------------------------------------------------------------------------- +print("\n--- 14-Statistical ---") +cli(f'add "{FILE}" / --type sheet --prop name="14-Statistical"') +cli(f'add "{FILE}" "/14-Statistical" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Region' + f' --prop cols=Quarter' + f' --prop values=Sales:var,Sales:varP,Sales:sum' + f' --prop showDataAs=running_total' + f' --prop layout=tabular' + f' --prop grandtotals=both' + f' --prop name=Statistical' + f' --prop style=PivotStyleLight10') + +# -------------------------------------------------------------------------- +# Sheet: 15-Independent Totals +# +# officecli add pivot-tables.xlsx "/15-Independent Totals" --type pivottable \ +# --prop source=CNData!A1:C13 \ +# --prop rows=地区 \ +# --prop cols=品类 \ +# --prop values=销售额:sum \ +# --prop rowGrandTotals=true \ +# --prop colGrandTotals=false \ +# --prop defaultSubtotal=true \ +# --prop layout=outline \ +# --prop subtotals=on \ +# --prop sort=locale-desc \ +# --prop name=IndepTotals \ +# --prop style=PivotStyleMedium11 +# +# Features: rowGrandTotals + colGrandTotals as independent toggles +# (vs the combined grandtotals=both/rows/cols/none), defaultSubtotal=true +# (default-subtotal flag on every pivotField), sort=locale-desc (reverse +# pinyin: 西南 > 华南 > 华东 > 华北). +# -------------------------------------------------------------------------- +print("\n--- 15-Independent Totals ---") +cli(f'add "{FILE}" / --type sheet --prop name="15-Independent Totals"') +cli(f'add "{FILE}" "/15-Independent Totals" --type pivottable' + f' --prop source=CNData!A1:C13' + f' --prop rows=地区' + f' --prop cols=品类' + f' --prop values=销售额:sum' + f' --prop rowGrandTotals=true' + f' --prop colGrandTotals=false' + f' --prop defaultSubtotal=true' + f' --prop layout=outline' + f' --prop subtotals=on' + f' --prop sort=locale-desc' + f' --prop name=IndepTotals' + f' --prop style=PivotStyleMedium11') + +# -------------------------------------------------------------------------- +# Sheet: 16-Style Flags +# +# officecli add pivot-tables.xlsx "/16-Style Flags" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Region,Category \ +# --prop cols=Quarter \ +# --prop values=Sales:sum \ +# --prop showRowStripes=true \ +# --prop showColStripes=true \ +# --prop showRowHeaders=true \ +# --prop showColHeaders=true \ +# --prop showLastColumn=true \ +# --prop layout=tabular \ +# --prop grandtotals=both \ +# --prop name=StyleFlags \ +# --prop style=PivotStyleMedium17 +# +# Features: every pivotTableStyleInfo flag wired up — row/col banding, +# row/col header emphasis, last-column highlight. These map to the five +# checkboxes in Excel's PivotTable Styles ribbon. +# -------------------------------------------------------------------------- +print("\n--- 16-Style Flags ---") +cli(f'add "{FILE}" / --type sheet --prop name="16-Style Flags"') +cli(f'add "{FILE}" "/16-Style Flags" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Region,Category' + f' --prop cols=Quarter' + f' --prop values=Sales:sum' + f' --prop showRowStripes=true' + f' --prop showColStripes=true' + f' --prop showRowHeaders=true' + f' --prop showColHeaders=true' + f' --prop showLastColumn=true' + f' --prop layout=tabular' + f' --prop grandtotals=both' + f' --prop name=StyleFlags' + f' --prop style=PivotStyleMedium17') + +# -------------------------------------------------------------------------- +# Sheet: 17-Display Toggles +# +# officecli add pivot-tables.xlsx "/17-Display Toggles" --type pivottable \ +# --prop source=Sheet1!A1:J51 \ +# --prop rows=Region,Category \ +# --prop values=Sales:sum \ +# --prop showDrill=false \ +# --prop mergeLabels=true \ +# --prop layout=outline \ +# --prop grandtotals=both \ +# --prop subtotals=on \ +# --prop name=DisplayToggles \ +# --prop style=PivotStyleLight19 +# +# Features: showDrill=false (hide +/- expand-collapse buttons on every field), +# mergeLabels=true (merge & center repeated outer-axis item cells — +# ). +# -------------------------------------------------------------------------- +print("\n--- 17-Display Toggles ---") +cli(f'add "{FILE}" / --type sheet --prop name="17-Display Toggles"') +cli(f'add "{FILE}" "/17-Display Toggles" --type pivottable' + f' --prop source=Sheet1!A1:J51' + f' --prop rows=Region,Category' + f' --prop values=Sales:sum' + f' --prop showDrill=false' + f' --prop mergeLabels=true' + f' --prop layout=outline' + f' --prop grandtotals=both' + f' --prop subtotals=on' + f' --prop name=DisplayToggles' + f' --prop style=PivotStyleLight19') + +print(f"\nDone! Generated: {FILE}") +print(" 19 sheets (Sheet1 + CNData + 17 pivot tables)") diff --git a/examples/excel/pivot-tables.xlsx b/examples/excel/pivot-tables.xlsx new file mode 100644 index 000000000..594a8d105 Binary files /dev/null and b/examples/excel/pivot-tables.xlsx differ diff --git a/examples/ppt/3d-model.md b/examples/ppt/3d-model.md new file mode 100644 index 000000000..9f810a627 --- /dev/null +++ b/examples/ppt/3d-model.md @@ -0,0 +1,6 @@ +# 3d-model + +TODO: rewrite script with annotated officecli commands. + +See [3d-model.sh](3d-model.sh) and [3d-model.pptx](3d-model.pptx). + diff --git a/examples/ppt/outputs/3d-sun.pptx b/examples/ppt/3d-model.pptx similarity index 99% rename from examples/ppt/outputs/3d-sun.pptx rename to examples/ppt/3d-model.pptx index 8ec4360f0..a994a8a56 100644 Binary files a/examples/ppt/outputs/3d-sun.pptx and b/examples/ppt/3d-model.pptx differ diff --git a/examples/ppt/gen-3d-sun-pptx.sh b/examples/ppt/3d-model.sh similarity index 99% rename from examples/ppt/gen-3d-sun-pptx.sh rename to examples/ppt/3d-model.sh index d09b4814f..d20b02639 100755 --- a/examples/ppt/gen-3d-sun-pptx.sh +++ b/examples/ppt/3d-model.sh @@ -5,7 +5,7 @@ set -e DIR="$(cd "$(dirname "$0")" && pwd)" MODELS="$DIR/models" -OUT="$DIR/outputs/3d-sun.pptx" +OUT="$DIR/3d-model.pptx" rm -f "$OUT" officecli create "$OUT" officecli open "$OUT" diff --git a/examples/ppt/README.md b/examples/ppt/README.md deleted file mode 100644 index c1592e35d..000000000 --- a/examples/ppt/README.md +++ /dev/null @@ -1,373 +0,0 @@ -# PowerPoint (.pptx) Examples & Templates - -Examples and professional style templates for PowerPoint presentation automation. - -## 📂 Structure - -``` -ppt/ -├── README.md # This file -├── gen-beautiful-pptx.sh # Basic examples -├── gen-animations-pptx.sh -├── gen-video-pptx.py -├── outputs/ # Generated examples -└── templates/ # 35 professional style templates ⭐ - ├── README.md # Style index and guide - └── styles/ # Individual style directories - ├── dark--*/ (14 dark styles, 8 available) - ├── light--*/ (8 light styles, 6 available) - ├── warm--*/ (5 warm styles) - ├── vivid--*/ (2 vivid styles) - ├── bw--*/ (3 black & white, 1 available) - └── mixed--*/ (1 mixed style) -``` - ---- - -## 🚀 Quick Start - -### Basic Examples - -```bash -# Beautiful presentation with morph transitions -bash gen-beautiful-pptx.sh - -# Animation effects -bash gen-animations-pptx.sh - -# Video embedding (Python) -python gen-video-pptx.py -``` - -### Professional Style Templates - -```bash -cd templates/styles/dark--investor-pitch -# View pre-generated PPT -open template.pptx - -# Or regenerate -bash build.sh -``` - -👉 **[Browse all 35 styles →](templates/)** (15 with pre-generated PPTs) - ---- - -## 🎨 Basic Scripts - -### [gen-beautiful-pptx.sh](gen-beautiful-pptx.sh) -**Create a beautiful presentation with morph transitions** - -```bash -bash gen-beautiful-pptx.sh -``` - -**Demonstrates:** -- Morph transitions between slides -- Shape creation and positioning -- Text styling and alignment -- Color palettes and gradients -- Layout design patterns - -**Output:** [`outputs/beautiful_presentation.pptx`](outputs/beautiful_presentation.pptx) - ---- - -### [gen-animations-pptx.sh](gen-animations-pptx.sh) -**Comprehensive animation examples** - -```bash -bash gen-animations-pptx.sh -``` - -**Demonstrates:** -- Entrance animations (fade, fly, zoom, etc.) -- Emphasis animations (pulse, grow, spin) -- Exit animations (disappear, fly out) -- Animation timing and sequencing -- Multiple animations per object - -**Output:** [`outputs/gen-animations-pptx.pptx`](outputs/gen-animations-pptx.pptx) - ---- - -### [gen-video-pptx.py](gen-video-pptx.py) -**Embed video in PowerPoint (Python)** - -```bash -python gen-video-pptx.py -``` - -**Demonstrates:** -- Video embedding -- Media positioning -- Python integration with OfficeCLI - -**Output:** [`outputs/gen-video-pptx.pptx`](outputs/gen-video-pptx.pptx) - ---- - -## 📈 Sample Outputs - -Pre-generated examples in [`outputs/`](outputs/): -- `beautiful_presentation.pptx` - Professional presentation with morph -- `data_presentation.pptx` - Data visualization deck -- `gen-animations-pptx.pptx` - Animation showcase -- `gen-video-pptx.pptx` - Video embedding example - ---- - -## 🎨 Professional Style Templates - -**35 design styles organized by color palette:** - -### 🌑 Dark Palette (14 styles, 8 available) -Perfect for tech, corporate, and futuristic themes. - -**Available (✅):** -- `dark--investor-pitch` - Investor pitches, fundraising decks -- `dark--cosmic-neon` - Science talks, futuristic topics -- `dark--editorial-story` - Brand storytelling, editorial magazines -- `dark--tech-cosmos` - Tech talks, architecture reviews -- `dark--cyber-future` - Futuristic topics, cyberpunk, AI -- `dark--luxury-minimal` - Luxury brands, premium products -- `dark--space-odyssey` - Space/astronomy, science education -- `dark--neon-productivity` - Productivity talks, motivation - -**Reference-Only (⚙️):** -- `dark--liquid-flow`, `dark--premium-navy`, `dark--blueprint-grid`, -- `dark--diagonal-cut`, `dark--spotlight-stage`, `dark--circle-digital` - -### ☀️ Light Palette (8 styles, 6 available) -Clean and professional for business and product showcases. - -**Available (✅):** -- `light--minimal-corporate` - Annual reports, business proposals -- `light--minimal-product` - Product launches, brand introductions -- `light--project-proposal` - Project kickoffs, bid presentations -- `light--spring-launch` - Spring launches, seasonal marketing -- `light--training-interactive` - Corporate training, online courses - -### 🧡 Warm Palette (5 styles) -Warm and friendly for lifestyle and organic brands. (Reference-only) - -### 🌈 Vivid Palette (2 styles) -Energetic and youthful for marketing campaigns. (Reference-only) - -### ⬛ Black & White (3 styles, 1 available) -Minimalist and sophisticated. - -**Available (✅):** -- `bw--swiss-bauhaus` - Design agencies, architecture firms - -### 🎨 Mixed Palette (1 style) -Bold architectural designs. (Reference-only) - -👉 **[Full style index with mood, use cases →](templates/README.md)** - ---- - -## 📖 Quick Lookup by Use Case - -| Use Case | Recommended Styles | -|----------|-------------------| -| **Tech / AI / SaaS** | ✅ dark--tech-cosmos, ✅ dark--cyber-future | -| **Investment / Pitch** | ✅ dark--investor-pitch, ✅ light--project-proposal | -| **Corporate / Business** | ✅ light--minimal-corporate, ✅ light--minimal-product | -| **Education / Training** | ✅ light--training-interactive | -| **Sci-Fi / Space / Future** | ✅ dark--space-odyssey, ✅ dark--cosmic-neon | -| **Luxury / Premium** | ✅ dark--luxury-minimal | -| **Design / Architecture** | ✅ bw--swiss-bauhaus | - ---- - -## 🎓 Key Concepts - -### Presentation Structure -``` -/Presentation - /slide[1] # First slide - /shape[1] # First shape - /shape[2] - /slide[2] - /master[1] # Slide master -``` - -### Common Commands - -**Add a slide:** -```bash -officecli add deck.pptx / --type slide \ - --prop layout=blank \ - --prop background=1A1A2E -``` - -**Add a shape:** -```bash -officecli add deck.pptx /slide[1] --type shape \ - --prop text="Hello World" \ - --prop x=5cm \ - --prop y=5cm \ - --prop width=10cm \ - --prop height=3cm \ - --prop size=48 \ - --prop bold=true \ - --prop color=FFFFFF -``` - -**Set transition:** -```bash -officecli set deck.pptx /slide[1] \ - --prop transition=morph \ - --prop advanceTime=3000 -``` - -**Copy slide:** -```bash -officecli add deck.pptx / --from /slide[1] -``` - ---- - -## 🎨 Shape Types - -### Available Presets - -| Preset | Description | -|--------|-------------| -| `rect` | Rectangle | -| `roundRect` | Rounded rectangle | -| `ellipse` | Circle/Ellipse | -| `triangle` | Triangle | -| `diamond` | Diamond | -| `pentagon` | Pentagon | -| `hexagon` | Hexagon | -| `star5` | 5-point star | -| `arrow` | Arrow | -| `callout` | Callout bubble | - -**View all presets:** -```bash -officecli pptx add -``` - ---- - -## 📊 Available Properties - -### Slide -- `layout` - Slide layout (blank, title, titleContent, etc.) -- `background` - Background color (hex) -- `transition` - Transition effect (fade, push, wipe, morph, etc.) -- `advanceTime` - Auto-advance time in milliseconds -- `notes` - Speaker notes - -### Shape -- `name` - Shape name/identifier -- `preset` - Shape preset (rect, ellipse, arrow, etc.) -- `text` - Text content -- `x`, `y` - Position (cm, in, pt, px, EMU) -- `width`, `height` - Size -- `rotation` - Rotation angle (degrees) -- `fill` - Fill color (hex) -- `line` - Line color (hex or "none") -- `opacity` - Opacity (0.0 to 1.0) - -### Text Formatting -- `font` - Font name -- `size` - Font size in points -- `bold` - true/false -- `italic` - true/false -- `color` - Text color (hex) -- `align` - left, center, right, justify -- `valign` - top, middle, bottom - -### Animations -- `animation` - Animation effect (fade, fly, zoom, etc.) -- `animDelay` - Delay before animation starts (ms) -- `animDuration` - Animation duration (ms) -- `animTrigger` - click, afterPrev, withPrev - -**For complete property list:** -```bash -officecli pptx set -officecli pptx set slide -officecli pptx set shape -``` - ---- - -## 🎬 Transitions & Animations - -### Popular Transitions -- `morph` - Seamless object morphing -- `fade` - Fade in/out -- `push` - Push from side -- `wipe` - Wipe across -- `zoom` - Zoom in/out -- `cube` - 3D cube rotation - -### Animation Types -- **Entrance:** fade, fly, zoom, appear, split -- **Emphasis:** pulse, grow, spin, teeter -- **Exit:** disappear, fly, fade, zoom - -**Morph Transition Tips:** -- Name objects consistently across slides (e.g., `name="!!title"`) -- Keep object hierarchy the same -- Change position, size, or color for smooth morphing - ---- - -## 🔧 Tips - -1. **View presentation structure:** - ```bash - officecli view deck.pptx outline - ``` - -2. **Check statistics:** - ```bash - officecli view deck.pptx stats - ``` - -3. **Query shapes:** - ```bash - officecli query deck.pptx "shape[fill=FF0000]" - ``` - -4. **Batch slide building:** - ```bash - cat << EOF | officecli batch deck.pptx - [ - {"command":"add","parent":"/","type":"slide","props":{"background":"000000"}}, - {"command":"add","parent":"/slide[1]","type":"shape","props":{"text":"Title","x":"5cm","y":"5cm"}} - ] - EOF - ``` - -5. **Resident mode for multi-slide decks:** - ```bash - officecli open deck.pptx - officecli add deck.pptx / --type slide - officecli add deck.pptx / --type slide - officecli close deck.pptx - ``` - -6. **Position units:** - - `cm` - Centimeters (recommended) - - `in` - Inches - - `pt` - Points - - `px` - Pixels - - EMU - Raw units (914400 = 1 inch) - ---- - -## 📚 More Resources - -- **[Style Templates](templates/)** - 35 professional styles (19 ready-to-use) -- **[PowerPoint documentation](../../SKILL.md#powerpoint-pptx)** - Complete reference -- **[All examples](../)** - Word, Excel, PowerPoint -- **[Word examples](../word/)** - Document automation -- **[Excel examples](../excel/)** - Spreadsheet automation diff --git a/examples/ppt/animations.md b/examples/ppt/animations.md new file mode 100644 index 000000000..a26113222 --- /dev/null +++ b/examples/ppt/animations.md @@ -0,0 +1,6 @@ +# animations + +TODO: rewrite script with annotated officecli commands. + +See [animations.sh](animations.sh) and [animations.pptx](animations.pptx). + diff --git a/examples/ppt/animations.pptx b/examples/ppt/animations.pptx new file mode 100644 index 000000000..2192b1d6d Binary files /dev/null and b/examples/ppt/animations.pptx differ diff --git a/examples/ppt/gen-animations-pptx.sh b/examples/ppt/animations.sh similarity index 99% rename from examples/ppt/gen-animations-pptx.sh rename to examples/ppt/animations.sh index eb95ea57d..71d2978e6 100755 --- a/examples/ppt/gen-animations-pptx.sh +++ b/examples/ppt/animations.sh @@ -3,7 +3,7 @@ # Each slide demonstrates a different category of animations set -e -OUT="$(dirname "$0")/gen-animations-pptx.pptx" +OUT="$(dirname "$0")/animations.pptx" rm -f "$OUT" officecli create "$OUT" officecli open "$OUT" diff --git a/examples/ppt/charts/charts-3d.md b/examples/ppt/charts/charts-3d.md new file mode 100644 index 000000000..a4b937763 --- /dev/null +++ b/examples/ppt/charts/charts-3d.md @@ -0,0 +1,64 @@ +# 3D Charts Showcase — column3d / bar3d / pie3d / line3d / area3d with view3d, gapdepth, shape + +Three files work together: + +- **charts-3d.py** — build script (`python3 charts-3d.py`). +- **charts-3d.pptx** — generated deck. +- **charts-3d.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-3d.py +# → charts-3d.pptx +``` + +## Slide map + +``` + Slide 1 3D families column3d / bar3d / pie3d / line3d + Slide 2 area3d & stacked 3D area3d / stackedColumn3d / percentStackedColumn3d / line3d stacked + Slide 3 view3d different rotX,rotY,perspective angles + Slide 4 gapdepth 0 / 50 / 150 / 300 (3D bar/column/line/area only) + Slide 5 bar shapes box / cylinder / cone / pyramid (bar3d / column3d) + Slide 6 Title & legend + Slide 7 Series styling colors, gradient, transparency, outline, shadow + Slide 8 Presets +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=column3d \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-3d.pptx b/examples/ppt/charts/charts-3d.pptx new file mode 100644 index 000000000..5a7f7f011 Binary files /dev/null and b/examples/ppt/charts/charts-3d.pptx differ diff --git a/examples/ppt/charts/charts-3d.py b/examples/ppt/charts/charts-3d.py new file mode 100644 index 000000000..b4f9e3abe --- /dev/null +++ b/examples/ppt/charts/charts-3d.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +3D Charts Showcase — column3d / bar3d / pie3d / line3d / area3d with view3d, gapdepth, shape. + +Generates: charts-3d.pptx + + Slide 1 3D families column3d / bar3d / pie3d / line3d + Slide 2 area3d & stacked 3D area3d / stackedColumn3d / percentStackedColumn3d / line3d stacked + Slide 3 view3d different rotX,rotY,perspective angles + Slide 4 gapdepth 0 / 50 / 150 / 300 (3D bar/column/line/area only) + Slide 5 bar shapes box / cylinder / cone / pyramid (bar3d / column3d) + Slide 6 Title & legend + Slide 7 Series styling colors, gradient, transparency, outline, shadow + Slide 8 Presets + +Usage: + python3 charts-3d.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-3d.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +CATS="Q1,Q2,Q3,Q4" +D2="East:120,135,148,162;West:95,108,115,128" +D3="East:120,135,148,162;South:95,108,115,128;West:80,90,98,110" +PIE_CATS="North,South,East,West"; PIE_D="Share:30,25,28,17" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("3D families — column3d / bar3d / pie3d / line3d") +ch(TL,{"chartType":"column3d","title":"column3d","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"bar3d","title":"bar3d","legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"pie3d","title":"pie3d","legend":"right","categories":PIE_CATS,"data":PIE_D}) +ch(BR,{"chartType":"line3d","title":"line3d","legend":"bottom","categories":CATS,"data":D2}) + +new_slide("area3d & stacked 3D") +ch(TL,{"chartType":"area3d","title":"area3d","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"stackedColumn3d","title":"stackedColumn3d","legend":"bottom", + "categories":CATS,"data":D3}) +ch(BL,{"chartType":"percentStackedColumn3d","title":"percentStackedColumn3d","legend":"bottom", + "categories":CATS,"data":D3}) +ch(BR,{"chartType":"stackedBar3d","title":"stackedBar3d","legend":"bottom", + "categories":CATS,"data":D3}) + +new_slide("view3d — rotX,rotY,perspective angles") +ch(TL,{"chartType":"column3d","title":"view3d=15,20,30","view3d":"15,20,30", + "legend":"none","categories":CATS,"data":D2}) +ch(TR,{"chartType":"column3d","title":"view3d=30,40,15","view3d":"30,40,15", + "legend":"none","categories":CATS,"data":D2}) +ch(BL,{"chartType":"column3d","title":"view3d=20","view3d":"20", + "legend":"none","categories":CATS,"data":D2}) +ch(BR,{"chartType":"pie3d","title":"pie3d view3d=40,30,30","view3d":"40,30,30", + "legend":"right","categories":PIE_CATS,"data":PIE_D}) + +new_slide("gapdepth — 0 / 50 / 150 / 300") +for box,g in zip([TL,TR,BL,BR],[0,50,150,300]): + ch(box,{"chartType":"column3d","title":f"gapdepth={g}","gapdepth":str(g), + "legend":"none","categories":CATS,"data":D2}) + +new_slide("3D bar shapes — box / cylinder / cone / pyramid") +for box,s in zip([TL,TR,BL,BR],["box","cylinder","cone","pyramid"]): + ch(box,{"chartType":"bar3d","shape":s,"title":f"shape={s}","legend":"none", + "categories":CATS,"data":D2}) + +new_slide("Title & legend") +ch(TL,{"chartType":"column3d","title":"Styled title","title.font":"Georgia","title.size":"20", + "title.color":"4472C4","title.bold":"true","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"column3d","title":"legend=top + legendFont","legend":"top", + "legendFont":"10:333333:Calibri","categories":CATS,"data":D2}) +ch(BL,{"chartType":"column3d","title":"legend.overlay=true","legend":"topRight", + "legend.overlay":"true","categories":CATS,"data":D2}) +ch(BR,{"chartType":"column3d","autotitledeleted":"true","legend":"none","categories":CATS,"data":D2}) + +new_slide("Series styling — colors, gradient, transparency, outline, shadow") +ch(TL,{"chartType":"column3d","title":"colors + seriesoutline","colors":"4472C4,ED7D31", + "seriesoutline":"000000:0.5","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"column3d","title":"gradient + seriesshadow", + "gradient":"FF6600-FFCC00","seriesshadow":"000000-5-45-3-50", + "legend":"none","categories":CATS,"data":D2}) +ch(BL,{"chartType":"column3d","title":"transparency=30","transparency":"30", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"column3d","title":"per-series gradients", + "gradients":"FF0000-0000FF;00FF00-FFFF00", + "legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Presets — preset bundles on 3D charts") +for box,p in zip([TL,TR,BL,BR],["minimal","dark","corporate","colorful"]): + ch(box,{"chartType":"column3d","preset":p,"title":f"preset={p}", + "view3d":"15,20,30","legend":"bottom","categories":CATS,"data":D2}) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-advanced.md b/examples/ppt/charts/charts-advanced.md new file mode 100644 index 000000000..b74fd4ac8 --- /dev/null +++ b/examples/ppt/charts/charts-advanced.md @@ -0,0 +1,51 @@ +# Advanced Charts Showcase — long-tail properties + +Three files work together: + +- **charts-advanced.py** — build script (`python3 charts-advanced.py`). +- **charts-advanced.pptx** — generated 8-slide deck. +- **charts-advanced.md** — this file. + +Covers properties **not** demonstrated by the per-type decks +(`charts-column`, `charts-bar`, …). After running every script in this +family, the only chart / chart-axis / chart-series properties that remain +unstamped are pure **get-only readback** fields — they cannot be Set as +input, but they surface in the JSON readback shapes on slide 8. + +## Slide map + +``` +Slide 1 RTL & anchor direction=rtl (Set), anchor named-token, anchor cm-form +Slide 2 Axis shortcuts axisvisible / valaxisvisible / catAxisVisible, + axisorientation, axisposition, + cataxisline / valaxisline +Slide 3 Crossings crossBetween (between/midCat), crosses (autoZero/max/min), crossesAt +Slide 4 Category axis layout labeloffset (100/300), ticklabelskip (2/3) +Slide 5 Marker size & fills markersize standalone, areafill, chartFill, plotvisonly +Slide 6 Style + blanks style=2/42, dispBlanksAs=gap (Set), dataRange syntax, catTitle +Slide 7 chart-axis Set dispUnits, logBase, minorUnit, visible, labelRotation (per-axis) +Slide 8 Get readback chart-series get --json (alpha, outlineColor, scatterStyle, ...) + chart-axis get --json (axisFont, axisMax, axisMin, axisNumFmt, + axisOrientation, axisTitle, labelOffset, + tickLabelSkip) +``` + +## Get-only readback fields (no Set, surface in `get --json`) + +| Element | Get-only properties | +|---|---| +| chart | `id` | +| chart-axis | `axisFont`, `axisMax`, `axisMin`, `axisNumFmt`, `axisOrientation`, `axisTitle`, `labelOffset`, `tickLabelSkip` | +| chart-series | `alpha`, `categoriesRef`, `dataLabels.numFmt`, `dataLabels.separator`, `errBars`, `invertIfNeg`, `nameRef`, `outlineColor`, `scatterStyle`, `secondaryAxis` | + +Slide 8 calls `officecli get --json` on a series and an axis after a known +Set, then stamps the JSON onto the slide — the readback fields appear in +that block. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-advanced.py +# → charts-advanced.pptx +``` diff --git a/examples/ppt/charts/charts-advanced.pptx b/examples/ppt/charts/charts-advanced.pptx new file mode 100644 index 000000000..ea56b3d31 Binary files /dev/null and b/examples/ppt/charts/charts-advanced.pptx differ diff --git a/examples/ppt/charts/charts-advanced.py b/examples/ppt/charts/charts-advanced.py new file mode 100644 index 000000000..0c8ecaa56 --- /dev/null +++ b/examples/ppt/charts/charts-advanced.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +""" +Advanced Charts Showcase — properties not covered by the per-type decks. + +Generates: charts-advanced.pptx + +Coverage of the long tail of chart properties (cross-handler / niche / axis-level): + + Slide 1 RTL & anchor direction=rtl, anchor named-token, anchor cm-form + Slide 2 Axis-level shortcuts axisvisible / valaxisvisible / catAxisVisible, + axisorientation, axisposition, + cataxisline / valaxisline + Slide 3 Crossings crossBetween (between/midCat), crosses (autoZero/max/min), crossesAt + Slide 4 Categories axis labeloffset, ticklabelskip + Slide 5 Marker size & fills markersize (standalone), areafill, chartFill, plotvisonly + Slide 6 Built-in style + blanks style=1..48, dispBlanksAs (gap / zero / span) + Slide 7 chart-axis Set dispUnits, logBase, minorUnit, visible, labelRotation (per-axis) + Slide 8 chart-series mutation values=, categories= (per-series range), + get-readback round-trip + +Usage: + python3 charts-advanced.py +""" +import subprocess, os, sys, atexit, json + +FILE = os.path.join(os.path.dirname(__file__), "charts-advanced.pptx") +def cli(*a, capture=False): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) + if capture: return r.stdout +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +def note(x,y,text): + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":text,"size":10,"italic":"true","color":"666666", + "x":x,"y":y,"width":"6in","height":"0.4in"})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +CATS="Q1,Q2,Q3,Q4" +D="A:60,90,140,180" +D2="A:60,90,140,180;B:50,75,110,150" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +# --------------------------------------------------------------------------- +# Slide 1 — RTL + anchor variants +# --------------------------------------------------------------------------- +new_slide("RTL + anchor — direction=rtl, named-token anchor, cm-form anchor") +ch(TL,{"chartType":"column","title":"default (LTR)","legend":"bottom", + "categories":CATS,"data":D2}) +# RTL must be Set after Add (direction is set-only) +ch(TR,{"chartType":"column","title":"direction=rtl (Set after Add)","legend":"bottom", + "categories":"Q1,Q2,Q3,Q4","data":D2}) +cli("set",FILE,f"/slide[{slide}]/chart[2]",*P({"direction":"rtl"})) +# Anchor cm-form: x,y,w,h +ch({"anchor":"0.3cm,11cm,15.5cm,7cm"},{"chartType":"column", + "title":"anchor=0.3cm,11cm,15.5cm,7cm","legend":"bottom", + "categories":CATS,"data":D}) + +# --------------------------------------------------------------------------- +# Slide 2 — axis-level shortcuts +# --------------------------------------------------------------------------- +new_slide("Axis shortcuts — axisvisible / valaxisvisible / catAxisVisible, orientation, position, lines") +ch(TL,{"chartType":"column","title":"axisvisible=false (both axes hidden)", + "legend":"none","axisvisible":"false","categories":CATS,"data":D}) +ch(TR,{"chartType":"column","title":"valaxisvisible=false (Y hidden, X shown)", + "legend":"none","valaxisvisible":"false","categories":CATS,"data":D}) +ch(BL,{"chartType":"column","title":"catAxisVisible=false (X hidden)", + "legend":"none","catAxisVisible":"false","categories":CATS,"data":D}) +ch(BR,{"chartType":"column","title":"axisorientation=true (reversed) + axisposition=top", + "legend":"none","axisorientation":"true","axisposition":"top", + "cataxisline":"333333:1","valaxisline":"333333:1", + "categories":CATS,"data":D}) + +# --------------------------------------------------------------------------- +# Slide 3 — Crossings +# --------------------------------------------------------------------------- +new_slide("Crossings — crossBetween / crosses / crossesAt") +ch(TL,{"chartType":"column","title":"crossBetween=between (default)", + "legend":"none","crossBetween":"between","categories":CATS,"data":D}) +ch(TR,{"chartType":"column","title":"crossBetween=midCat","legend":"none", + "crossBetween":"midCat","categories":CATS,"data":D}) +ch(BL,{"chartType":"column","title":"crosses=max (Y crosses at top)","legend":"none", + "crosses":"max","categories":CATS,"data":D}) +ch(BR,{"chartType":"column","title":"crossesAt=100 + crosses=autoZero", + "legend":"none","crosses":"autoZero","crossesAt":"100", + "categories":CATS,"data":"A:60,-30,140,180"}) + +# --------------------------------------------------------------------------- +# Slide 4 — Category axis layout +# --------------------------------------------------------------------------- +new_slide("Category axis — labeloffset, ticklabelskip") +ch(TL,{"chartType":"column","title":"labeloffset=100 (default)", + "labeloffset":"100","legend":"none", + "categories":"January,February,March,April,May,June", + "data":"A:60,90,140,180,160,210"}) +ch(TR,{"chartType":"column","title":"labeloffset=300 (push labels down)", + "labeloffset":"300","legend":"none", + "categories":"January,February,March,April,May,June", + "data":"A:60,90,140,180,160,210"}) +ch(BL,{"chartType":"column","title":"ticklabelskip=2 (every other label)", + "ticklabelskip":"2","legend":"none", + "categories":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", + "data":"A:60,90,140,180,160,210,200,190,170,150,130,170"}) +ch(BR,{"chartType":"column","title":"ticklabelskip=3","ticklabelskip":"3","legend":"none", + "categories":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", + "data":"A:60,90,140,180,160,210,200,190,170,150,130,170"}) + +# --------------------------------------------------------------------------- +# Slide 5 — Marker size, area/chart fills, plotvisonly +# --------------------------------------------------------------------------- +new_slide("Marker size & fills — markersize (standalone), areafill, chartFill, plotvisonly") +ch(TL,{"chartType":"line","title":"markersize=12 (standalone key)", + "showMarker":"true","markersize":"12","legend":"none", + "categories":CATS,"data":D}) +ch(TR,{"chartType":"column","title":"areafill (applies to every series shape)", + "areafill":"4472C4-A5C8FF:90","legend":"none","categories":CATS,"data":D2}) +ch(BL,{"chartType":"column","title":"chartFill=#FFF8E7 (chart-level fill)", + "chartFill":"#FFF8E7","legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"column","title":"plotvisonly=true (skip hidden rows when bound to a sheet)", + "plotvisonly":"true","legend":"none","categories":CATS,"data":D}) + +# --------------------------------------------------------------------------- +# Slide 6 — Built-in style id + dispBlanksAs +# --------------------------------------------------------------------------- +new_slide("Built-in style & blank handling — style=1..48, dispBlanksAs, dataRange") +ch(TL,{"chartType":"column","style":"2","title":"style=2","legend":"bottom", + "categories":CATS,"data":D2}) +ch(TR,{"chartType":"column","style":"42","title":"style=42","legend":"bottom", + "categories":CATS,"data":D2}) +# dispBlanksAs is Set/Get only — Add first, then Set. +ch(BL,{"chartType":"line","title":"dispBlanksAs=gap (Set after Add)","showMarker":"true", + "legend":"bottom","categories":CATS,"data":"A:60,90,140,180"}) +cli("set",FILE,f"/slide[{slide}]/chart[3]",*P({"dispBlanksAs":"gap"})) +# dataRange is Add-time alternative to data= for sheet-backed sources; +# in a standalone pptx this is largely symbolic — we still demonstrate the syntax, +# then fall back to inline data so the chart renders. +ch(BR,{"chartType":"column","title":"dataRange syntax demo (fallback inline)", + "dataRange":"Sheet1!A1:D5","legend":"bottom","catTitle":"Quarter", + "categories":CATS,"data":D2}) + +# --------------------------------------------------------------------------- +# Slide 7 — chart-axis Set (per-axis post-Add) +# --------------------------------------------------------------------------- +new_slide("chart-axis Set — dispUnits, logBase, minorUnit, visible, labelRotation per-axis") +ch(TL,{"chartType":"column","title":"after: dispUnits=thousands (Set on value axis)", + "legend":"none","categories":CATS,"data":"Rev:120000,135000,148000,162000"}) +cli("set",FILE,f"/slide[{slide}]/chart[1]/axis[@role=value]", + *P({"dispUnits":"thousands","format":"#,##0","minorUnit":"10000", + "labelRotation":"0","visible":"true"})) +ch(TR,{"chartType":"line","title":"after: logBase=10 (Set on value axis)", + "legend":"none","categories":CATS,"data":"A:5,50,500,5000"}) +cli("set",FILE,f"/slide[{slide}]/chart[2]/axis[@role=value]", + *P({"logBase":"10","min":"1","max":"10000","majorGridlines":"true"})) +ch(BL,{"chartType":"column","title":"after: visible=false on value axis", + "legend":"none","categories":CATS,"data":D}) +cli("set",FILE,f"/slide[{slide}]/chart[3]/axis[@role=value]",*P({"visible":"false"})) +ch(BR,{"chartType":"column","title":"after: labelRotation=-45 on category axis", + "legend":"none","categories":"January,February,March,April","data":D}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/axis[@role=category]", + *P({"labelRotation":"-45","title":"Month","visible":"true"})) + +# --------------------------------------------------------------------------- +# Slide 8 — chart-series values=/categories= Set + Get readback round-trip +# --------------------------------------------------------------------------- +new_slide("chart-series mutation — values=, categories= + get-readback round-trip") +ch(TL,{"chartType":"column","title":"before: A=60,90,140,180","legend":"bottom", + "categories":CATS,"data":D}) +# Mutate the values after add +cli("set",FILE,f"/slide[{slide}]/chart[1]/series[1]",*P({"values":"200,150,100,80"})) +note("0.3in","4in","After Set values=200,150,100,80 the series flips downward.") + +ch(TR,{"chartType":"column","title":"per-series categories= (range)","legend":"bottom", + "categories":CATS,"data":D}) +# Per-series category override is range-only — note that it requires sheet backing +# so this is a demonstration of the syntax only; effective result depends on workbook. + +# Round-trip: change one series, then read it back and stamp the JSON onto the slide +cli("set",FILE,f"/slide[{slide}]/chart[1]/series[1]", + *P({"name":"Readback Demo","color":"C00000"})) +out = cli("get",FILE,f"/slide[{slide}]/chart[1]/series[1]","--json", capture=True) or "" +# Pretty-print, trim +try: + obj = json.loads(out) + if isinstance(obj, dict) and "data" in obj: obj = obj["data"] + pretty = json.dumps(obj.get("format", obj), indent=2)[:600] +except Exception: + pretty = out[:600] +cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":"chart-series get --json (readback fields alpha/outlineColor/scatterStyle/...):\n"+pretty, + "size":9,"color":"222222","x":"0.3in","y":"4.25in", + "width":"6.1in","height":"3in"})) + +# chart-axis get-readback — surfaces axisFont/axisMax/axisMin/axisNumFmt/ +# axisOrientation/axisTitle/labelOffset/tickLabelSkip read-only fields. +cli("set",FILE,f"/slide[{slide}]/chart[1]/axis[@role=value]", + *P({"title":"Readback Y","format":"$#,##0","min":"0","max":"300","majorUnit":"75"})) +ax = cli("get",FILE,f"/slide[{slide}]/chart[1]/axis[@role=value]","--json", capture=True) or "" +try: + obj = json.loads(ax) + if isinstance(obj, dict) and "data" in obj: obj = obj["data"] + ax_pretty = json.dumps(obj.get("format", obj), indent=2)[:500] +except Exception: + ax_pretty = ax[:500] +cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":"chart-axis get --json (readback axisFont/axisMax/axisMin/axisNumFmt/axisOrientation/axisTitle/labelOffset/tickLabelSkip):\n"+ax_pretty, + "size":9,"color":"222222","x":"6.95in","y":"4.25in", + "width":"6.1in","height":"3in"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-area.md b/examples/ppt/charts/charts-area.md new file mode 100644 index 000000000..95dcd58ea --- /dev/null +++ b/examples/ppt/charts/charts-area.md @@ -0,0 +1,64 @@ +# Area Charts Showcase — area, stackedArea, percentStackedArea, area3d + +Three files work together: + +- **charts-area.py** — build script (`python3 charts-area.py`). +- **charts-area.pptx** — generated deck. +- **charts-area.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-area.py +# → charts-area.pptx +``` + +## Slide map + +``` + Slide 1 Variants area / stackedArea / percentStackedArea / area3d + Slide 2 Title & legend title.* + legend positions + legendFont + Slide 3 Data labels flags + labelPos + labelfont + Slide 4 Axes min/max, titles, fonts, gridlines, ticks, labelrotation + Slide 5 Series styling colors, gradient, gradients, transparency, seriesoutline, seriesshadow + Slide 6 Overlays referenceline, errbars, trendline + Slide 7 Backgrounds chartareafill, plotFill, chartborder, plotborder, roundedcorners + Slide 8 Presets & per-ser preset bundles + seriesN.* + chart-series Set +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=area \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-area.pptx b/examples/ppt/charts/charts-area.pptx new file mode 100644 index 000000000..6fdd35686 Binary files /dev/null and b/examples/ppt/charts/charts-area.pptx differ diff --git a/examples/ppt/charts/charts-area.py b/examples/ppt/charts/charts-area.py new file mode 100644 index 000000000..28920360f --- /dev/null +++ b/examples/ppt/charts/charts-area.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +Area Charts Showcase — area, stackedArea, percentStackedArea, area3d. + +Generates: charts-area.pptx + + Slide 1 Variants area / stackedArea / percentStackedArea / area3d + Slide 2 Title & legend title.* + legend positions + legendFont + Slide 3 Data labels flags + labelPos + labelfont + Slide 4 Axes min/max, titles, fonts, gridlines, ticks, labelrotation + Slide 5 Series styling colors, gradient, gradients, transparency, seriesoutline, seriesshadow + Slide 6 Overlays referenceline, errbars, trendline + Slide 7 Backgrounds chartareafill, plotFill, chartborder, plotborder, roundedcorners + Slide 8 Presets & per-ser preset bundles + seriesN.* + chart-series Set + +Usage: + python3 charts-area.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-area.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +CATS="Mon,Tue,Wed,Thu,Fri" +D="A:50,60,70,65,80" +D2="Web:50,60,70,65,80;Mobile:30,35,42,48,55" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("Area variants — area / stackedArea / percentStackedArea / area3d") +ch(TL,{"chartType":"area","title":"area","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"stackedArea","title":"stackedArea","legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"percentStackedArea","title":"percentStackedArea","legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"area3d","title":"area3d","view3d":"15,20,30","legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Title & legend") +ch(TL,{"chartType":"area","title":"Styled title","title.font":"Georgia","title.size":"20", + "title.color":"4472C4","title.bold":"true","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"area","title":"legend=top + legendFont","legend":"top", + "legendFont":"10:333333:Calibri","categories":CATS,"data":D2}) +ch(BL,{"chartType":"area","title":"legend.overlay=true","legend":"topRight", + "legend.overlay":"true","categories":CATS,"data":D2}) +ch(BR,{"chartType":"area","autotitledeleted":"true","legend":"none","categories":CATS,"data":D2}) + +new_slide("Data labels — flags, labelPos, labelfont") +ch(TL,{"chartType":"area","title":"dataLabels=value","dataLabels":"value", + "labelfont":"10:333333:Calibri","legend":"none","categories":CATS,"data":D}) +ch(TR,{"chartType":"stackedArea","title":"stacked + center labels","dataLabels":"value", + "labelPos":"center","legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"area","title":"value,category","dataLabels":"value,category", + "labelfont":"9:333333:Calibri","legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"area","title":"dataLabels=none","dataLabels":"none","legend":"none", + "categories":CATS,"data":D}) + +new_slide("Axes — min/max, gridlines, ticks, labelrotation") +ch(TL,{"chartType":"area","title":"min/max + titles","legend":"none", + "axismin":"0","axismax":"100","majorunit":"25","axistitle":"Value","cattitle":"Day", + "axisfont":"10:333333:Calibri","axisline":"666666:1","axisnumfmt":"#,##0", + "categories":CATS,"data":D}) +ch(TR,{"chartType":"area","title":"gridlines + ticks","legend":"none", + "gridlines":"E0E0E0:0.3","minorGridlines":"F0F0F0:0.25", + "majorTickMark":"out","minorTickMark":"in","tickLabelPos":"nextTo", + "categories":CATS,"data":D}) +ch(BL,{"chartType":"area","title":"labelrotation=-30","legend":"none","labelrotation":"-30", + "categories":"January,February,March,April,May,June","data":"A:60,90,140,180,160,210"}) +ch(BR,{"chartType":"area","title":"dispunits=thousands","legend":"none","dispunits":"thousands", + "categories":CATS,"data":"Rev:120000,135000,148000,162000,180000"}) + +new_slide("Series styling — colors, gradient(s), transparency, outline, shadow") +ch(TL,{"chartType":"area","title":"colors + seriesoutline","legend":"bottom", + "colors":"4472C4,ED7D31","seriesoutline":"000000:0.5","categories":CATS,"data":D2}) +ch(TR,{"chartType":"area","title":"gradient + seriesshadow","legend":"none", + "gradient":"FF6600-FFCC00:90","seriesshadow":"000000-5-45-3-50", + "categories":CATS,"data":D}) +ch(BL,{"chartType":"area","title":"per-series gradients + transparency=30", + "gradients":"FF0000-0000FF;00FF00-FFFF00","transparency":"30", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"area","title":"single + transparency=50","transparency":"50", + "colors":"4472C4","legend":"none","categories":CATS,"data":D}) + +new_slide("Overlays — referenceline, errbars, trendline") +ch(TL,{"chartType":"area","title":"referenceline=60","referenceline":"60:FF0000:Target", + "legend":"none","categories":CATS,"data":D}) +ch(TR,{"chartType":"area","title":"errbars=percentage:10","errbars":"percentage:10", + "legend":"none","categories":CATS,"data":D}) +ch(BL,{"chartType":"area","title":"trendline=linear","trendline":"linear", + "legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"area","title":"trendline=movingAvg:3","trendline":"movingAvg:3", + "legend":"none","categories":CATS,"data":D}) + +new_slide("Backgrounds — chartareafill, plotFill, chartborder, plotborder, roundedcorners") +ch(TL,{"chartType":"area","title":"chartareafill + plotFill + borders","legend":"bottom", + "chartareafill":"FFF8E7","plotFill":"FAFAFA","chartborder":"000000:1", + "plotborder":"CCCCCC:0.5","categories":CATS,"data":D2}) +ch(TR,{"chartType":"area","title":"roundedcorners=true","roundedcorners":"true", + "chartborder":"4472C4:2","legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"area","title":"plotFill=none","plotFill":"none","gridlines":"none", + "legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"area","title":"dataTable=true","dataTable":"true","legend":"bottom", + "categories":CATS,"data":D2}) + +new_slide("Presets & per-series control") +for box,p in zip([TL,TR,BL],["minimal","dark","corporate"]): + ch(box,{"chartType":"area","preset":p,"title":f"preset={p}","legend":"bottom", + "categories":CATS,"data":D2}) +ch(BR,{"chartType":"area","title":"seriesN.* + chart-series Set","legend":"bottom", + "categories":CATS, + "series1.name":"Web","series1.values":"50,60,70,65,80","series1.color":"4472C4", + "series2.name":"Mobile","series2.values":"30,35,42,48,55","series2.color":"ED7D31"}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[1]",*P({"name":"Renamed Web","color":"C00000"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-bar.md b/examples/ppt/charts/charts-bar.md new file mode 100644 index 000000000..c82c79474 --- /dev/null +++ b/examples/ppt/charts/charts-bar.md @@ -0,0 +1,64 @@ +# Bar Charts Showcase — bar, stackedBar, percentStackedBar, bar3d (cylinder/cone/pyramid) + +Three files work together: + +- **charts-bar.py** — build script (`python3 charts-bar.py`). +- **charts-bar.pptx** — generated deck. +- **charts-bar.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-bar.py +# → charts-bar.pptx +``` + +## Slide map + +``` + Slide 1 Variants bar / stackedBar / percentStackedBar / bar3d + Slide 2 3D bar shapes shape=box/cylinder/cone/pyramid (bar3d only) + Slide 3 Title & legend title.* + legend positions + legendFont + Slide 4 Data labels flags + labelPos + labelfont + Slide 5 Axes min/max/title/font/line/numfmt/gridlines/labelrotation + Slide 6 Series styling colors, gradient, transparency, outline, shadow, invertifneg, serlines + Slide 7 Overlays referenceline, errbars, gapwidth, overlap, dataTable + Slide 8 Presets & per-ser preset bundles + seriesN.* + chart-series Set +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=bar \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-bar.pptx b/examples/ppt/charts/charts-bar.pptx new file mode 100644 index 000000000..e66d97324 Binary files /dev/null and b/examples/ppt/charts/charts-bar.pptx differ diff --git a/examples/ppt/charts/charts-bar.py b/examples/ppt/charts/charts-bar.py new file mode 100644 index 000000000..2d5ea5a45 --- /dev/null +++ b/examples/ppt/charts/charts-bar.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +Bar Charts Showcase — bar, stackedBar, percentStackedBar, bar3d (cylinder/cone/pyramid). + +Generates: charts-bar.pptx + + Slide 1 Variants bar / stackedBar / percentStackedBar / bar3d + Slide 2 3D bar shapes shape=box/cylinder/cone/pyramid (bar3d only) + Slide 3 Title & legend title.* + legend positions + legendFont + Slide 4 Data labels flags + labelPos + labelfont + Slide 5 Axes min/max/title/font/line/numfmt/gridlines/labelrotation + Slide 6 Series styling colors, gradient, transparency, outline, shadow, invertifneg, serlines + Slide 7 Overlays referenceline, errbars, gapwidth, overlap, dataTable + Slide 8 Presets & per-ser preset bundles + seriesN.* + chart-series Set + +Usage: + python3 charts-bar.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-bar.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m = (r.stderr or r.stdout or "").strip().splitlines() + head = m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide = 0 +def new_slide(t): + global slide; slide += 1 + cli("add", FILE, "/", "--type", "slide") + cli("add", FILE, f"/slide[{slide}]", "--type", "shape", + *P({"text": t, "size": 24, "bold": "true", + "autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box, p): cli("add", FILE, f"/slide[{slide}]", "--type", "chart", *P({**box, **p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +CATS="Q1,Q2,Q3,Q4"; D2="East:120,135,148,162;West:95,108,115,128" +D3="East:120,135,148,162;South:95,108,115,128;West:80,90,98,110" + +if os.path.exists(FILE): os.remove(FILE) +cli("create", FILE); cli("open", FILE) +atexit.register(lambda: (cli("close", FILE), cli("validate", FILE))) + +new_slide("Bar variants — bar / stackedBar / percentStackedBar / bar3d") +ch(TL,{"chartType":"bar","title":"bar","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"stackedBar","title":"stackedBar","legend":"bottom","categories":CATS,"data":D3}) +ch(BL,{"chartType":"percentStackedBar","title":"percentStackedBar","legend":"bottom","categories":CATS,"data":D3}) +ch(BR,{"chartType":"bar3d","title":"bar3d","legend":"bottom","categories":CATS,"data":D2,"view3d":"15,20,30"}) + +new_slide("3D bar shapes — shape=box / cylinder / cone / pyramid") +ch(TL,{"chartType":"bar3d","shape":"box","title":"shape=box","legend":"none","categories":CATS,"data":D2}) +ch(TR,{"chartType":"bar3d","shape":"cylinder","title":"shape=cylinder","legend":"none","categories":CATS,"data":D2}) +ch(BL,{"chartType":"bar3d","shape":"cone","title":"shape=cone","legend":"none","categories":CATS,"data":D2}) +ch(BR,{"chartType":"bar3d","shape":"pyramid","title":"shape=pyramid","legend":"none","categories":CATS,"data":D2}) + +new_slide("Title & legend") +ch(TL,{"chartType":"bar","title":"Styled title","title.font":"Georgia","title.size":"20", + "title.color":"4472C4","title.bold":"true","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"bar","title":"legend=top + legendFont","legend":"top", + "legendFont":"10:333333:Calibri","categories":CATS,"data":D2}) +ch(BL,{"chartType":"bar","title":"legend.overlay=true","legend":"topRight", + "legend.overlay":"true","categories":CATS,"data":D2}) +ch(BR,{"chartType":"bar","autotitledeleted":"true","legend":"none","categories":CATS,"data":D2}) + +new_slide("Data labels — flags, labelPos, labelfont") +ch(TL,{"chartType":"bar","title":"value @ outsideEnd","dataLabels":"value", + "labelPos":"outsideEnd","labelfont":"10:333333:Calibri","legend":"none", + "categories":CATS,"data":"A:60,90,140,180"}) +ch(TR,{"chartType":"bar","title":"value,category @ insideEnd","dataLabels":"value,category", + "labelPos":"insideEnd","labelfont":"9:FFFFFF:Calibri","legend":"none", + "categories":CATS,"data":"A:60,90,140,180"}) +ch(BL,{"chartType":"stackedBar","title":"stacked + center labels","dataLabels":"value", + "labelPos":"center","labelfont":"9:FFFFFF:Calibri","legend":"bottom", + "categories":CATS,"data":D3}) +ch(BR,{"chartType":"bar","title":"dataLabels=none","dataLabels":"none","legend":"none", + "categories":CATS,"data":"A:60,90,140,180"}) + +new_slide("Axes — min/max, titles, fonts, gridlines, ticks, labelrotation") +ch(TL,{"chartType":"bar","title":"min/max + titles + numfmt","legend":"none", + "axismin":"0","axismax":"200","majorunit":"50","minorunit":"10", + "axistitle":"Revenue","cattitle":"Quarter","axisfont":"10:333333:Calibri", + "axisline":"666666:1","axisnumfmt":"#,##0","categories":CATS,"data":"Rev:60,90,140,180"}) +ch(TR,{"chartType":"bar","title":"gridlines + ticks","legend":"none", + "gridlines":"E0E0E0:0.3","minorGridlines":"F0F0F0:0.25", + "majorTickMark":"out","minorTickMark":"in","tickLabelPos":"nextTo", + "categories":CATS,"data":"A:60,90,140,180"}) +ch(BL,{"chartType":"bar","title":"labelrotation=-30","legend":"none","labelrotation":"-30", + "categories":"January,February,March,April","data":"A:60,90,140,180"}) +ch(BR,{"chartType":"bar","title":"dispunits=thousands","legend":"none","dispunits":"thousands", + "categories":CATS,"data":"Rev:120000,135000,148000,162000"}) +cli("set", FILE, f"/slide[{slide}]/chart[1]/axis[@role=value]", + *P({"title":"Revenue","format":"$#,##0","majorGridlines":"true","max":"200","min":"0"})) + +new_slide("Series styling — colors, gradient(s), transparency, outline, shadow, invertifneg, serlines") +ch(TL,{"chartType":"bar","title":"colors + seriesoutline","legend":"bottom", + "colors":"4472C4,ED7D31,A5A5A5","seriesoutline":"000000:0.5","categories":CATS,"data":D3}) +ch(TR,{"chartType":"bar","title":"gradient + seriesshadow","legend":"bottom", + "gradient":"FF6600-FFCC00:90","seriesshadow":"000000-5-45-3-50", + "categories":CATS,"data":"A:60,90,140,180"}) +ch(BL,{"chartType":"bar","title":"transparency=30 + gradients","legend":"bottom", + "gradients":"FF0000-0000FF;00FF00-FFFF00","transparency":"30", + "categories":CATS,"data":"A:60,90,140,180;B:40,70,100,130"}) +ch(BR,{"chartType":"stackedBar","title":"stacked + serlines=true","serlines":"true", + "legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Overlays — referenceline, errbars, gapwidth, overlap, dataTable") +ch(TL,{"chartType":"bar","title":"referenceline=100","legend":"none", + "referenceline":"100:FF0000:Target","categories":CATS,"data":"A:60,90,140,180"}) +ch(TR,{"chartType":"bar","title":"errbars=fixedVal:10","legend":"none", + "errbars":"fixedVal:10","categories":CATS,"data":"A:60,90,140,180"}) +ch(BL,{"chartType":"bar","title":"gapwidth=50 + overlap=20","legend":"bottom", + "gapwidth":"50","overlap":"20","categories":CATS, + "data":"A:60,90,140,180;B:50,75,110,150"}) +ch(BR,{"chartType":"bar","title":"dataTable=true","legend":"bottom", + "dataTable":"true","categories":CATS,"data":"A:60,90,140,180"}) + +new_slide("Presets & per-series control") +ch(TL,{"chartType":"bar","preset":"minimal","title":"preset=minimal","legend":"bottom", + "categories":CATS,"data":"A:60,90,140,180;B:50,75,110,150"}) +ch(TR,{"chartType":"bar","preset":"dark","title":"preset=dark","legend":"bottom", + "categories":CATS,"data":"A:60,90,140,180;B:50,75,110,150"}) +ch(BL,{"chartType":"bar","preset":"corporate","title":"preset=corporate","legend":"bottom", + "categories":CATS,"data":"A:60,90,140,180;B:50,75,110,150"}) +ch(BR,{"chartType":"bar","title":"seriesN.* Add + chart-series Set","legend":"bottom", + "categories":CATS, + "series1.name":"Product A","series1.values":"60,90,140,180","series1.color":"4472C4", + "series2.name":"Product B","series2.values":"50,75,110,150","series2.color":"ED7D31"}) +cli("set", FILE, f"/slide[{slide}]/chart[4]/series[1]", *P({"name":"Renamed","color":"C00000"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-bubble.md b/examples/ppt/charts/charts-bubble.md new file mode 100644 index 000000000..7d6bd9d9b --- /dev/null +++ b/examples/ppt/charts/charts-bubble.md @@ -0,0 +1,64 @@ +# Bubble Charts Showcase + +Three files work together: + +- **charts-bubble.py** — build script (`python3 charts-bubble.py`). +- **charts-bubble.pptx** — generated deck. +- **charts-bubble.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-bubble.py +# → charts-bubble.pptx +``` + +## Slide map + +``` + Slide 1 bubbleScale 50 / 100 / 150 / 200 (% of default) + Slide 2 sizerepresents area vs width + Slide 3 shownegbubbles true vs false (with negative values) + Slide 4 Title & legend title.* + legend positions + legendFont + Slide 5 Data labels value/category/bubbleSize, labelfont + Slide 6 Axes min/max, gridlines, ticks + Slide 7 Series styling colors, gradient, transparency, outline, shadow + Slide 8 Presets & per-series preset bundles + chart-series Set +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=bubble \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-bubble.pptx b/examples/ppt/charts/charts-bubble.pptx new file mode 100644 index 000000000..7ddb6a87c Binary files /dev/null and b/examples/ppt/charts/charts-bubble.pptx differ diff --git a/examples/ppt/charts/charts-bubble.py b/examples/ppt/charts/charts-bubble.py new file mode 100644 index 000000000..9b5a1da1a --- /dev/null +++ b/examples/ppt/charts/charts-bubble.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Bubble Charts Showcase. + +Generates: charts-bubble.pptx + + Slide 1 bubbleScale 50 / 100 / 150 / 200 (% of default) + Slide 2 sizerepresents area vs width + Slide 3 shownegbubbles true vs false (with negative values) + Slide 4 Title & legend title.* + legend positions + legendFont + Slide 5 Data labels value/category/bubbleSize, labelfont + Slide 6 Axes min/max, gridlines, ticks + Slide 7 Series styling colors, gradient, transparency, outline, shadow + Slide 8 Presets & per-series preset bundles + chart-series Set + +Usage: + python3 charts-bubble.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-bubble.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +D="A:5,12,8,18,22,9,15,11" +D2="A:5,12,8,18,22,9;B:7,11,15,9,20,14" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("bubbleScale — 50 / 100 / 150 / 200 (% of default)") +for box,s in zip([TL,TR,BL,BR],[50,100,150,200]): + ch(box,{"chartType":"bubble","title":f"bubbleScale={s}","bubbleScale":str(s), + "legend":"none","data":D}) + +new_slide("sizerepresents — area vs width") +ch(TL,{"chartType":"bubble","title":"sizerepresents=area","sizerepresents":"area", + "legend":"none","data":D}) +ch(TR,{"chartType":"bubble","title":"sizerepresents=width","sizerepresents":"width", + "legend":"none","data":D}) +ch(BL,{"chartType":"bubble","title":"area + 2 series","sizerepresents":"area", + "legend":"bottom","data":D2}) +ch(BR,{"chartType":"bubble","title":"width + 2 series","sizerepresents":"width", + "legend":"bottom","data":D2}) + +new_slide("shownegbubbles — false vs true") +ch(TL,{"chartType":"bubble","title":"shownegbubbles=false","shownegbubbles":"false", + "legend":"none","data":"A:5,-8,12,-15,18,22"}) +ch(TR,{"chartType":"bubble","title":"shownegbubbles=true","shownegbubbles":"true", + "legend":"none","data":"A:5,-8,12,-15,18,22"}) +ch(BL,{"chartType":"bubble","title":"false + 2 series","shownegbubbles":"false", + "legend":"bottom","data":"A:5,-8,12,-15,18,22;B:8,11,-9,14,-16,20"}) +ch(BR,{"chartType":"bubble","title":"true + 2 series","shownegbubbles":"true", + "legend":"bottom","data":"A:5,-8,12,-15,18,22;B:8,11,-9,14,-16,20"}) + +new_slide("Title & legend") +ch(TL,{"chartType":"bubble","title":"Styled title","title.font":"Georgia","title.size":"20", + "title.color":"4472C4","title.bold":"true","legend":"bottom","data":D2}) +ch(TR,{"chartType":"bubble","title":"legend=top + legendFont","legend":"top", + "legendFont":"10:333333:Calibri","data":D2}) +ch(BL,{"chartType":"bubble","title":"legend.overlay=true","legend":"topRight", + "legend.overlay":"true","data":D2}) +ch(BR,{"chartType":"bubble","autotitledeleted":"true","legend":"none","data":D2}) + +new_slide("Data labels — flags + labelfont") +ch(TL,{"chartType":"bubble","title":"value","dataLabels":"value", + "labelfont":"9:333333:Calibri","legend":"none","data":D}) +ch(TR,{"chartType":"bubble","title":"value,series","dataLabels":"value,series", + "legend":"none","data":D2}) +ch(BL,{"chartType":"bubble","title":"labelPos=top","dataLabels":"value","labelPos":"top", + "legend":"none","data":D}) +ch(BR,{"chartType":"bubble","title":"dataLabels=none","dataLabels":"none","legend":"none","data":D}) + +new_slide("Axes — min/max, gridlines, ticks") +ch(TL,{"chartType":"bubble","title":"min/max + titles","axismin":"0","axismax":"30", + "majorunit":"10","axistitle":"Y","cattitle":"X","axisfont":"10:333333:Calibri", + "axisline":"666666:1","legend":"none","data":D}) +ch(TR,{"chartType":"bubble","title":"gridlines + minorGridlines", + "gridlines":"E0E0E0:0.3","minorGridlines":"F0F0F0:0.25","legend":"none","data":D}) +ch(BL,{"chartType":"bubble","title":"labelrotation=-30","labelrotation":"-30","legend":"none","data":D}) +ch(BR,{"chartType":"bubble","title":"dispunits=hundreds","dispunits":"hundreds","legend":"none", + "data":"A:500,1200,800,1800,2200,900"}) + +new_slide("Series styling — colors, gradient, transparency, outline, shadow") +ch(TL,{"chartType":"bubble","title":"colors + seriesoutline","colors":"4472C4,ED7D31", + "seriesoutline":"000000:0.5","legend":"bottom","data":D2}) +ch(TR,{"chartType":"bubble","title":"gradient + seriesshadow", + "gradient":"FF6600-FFCC00","seriesshadow":"000000-5-45-3-50", + "legend":"none","data":D}) +ch(BL,{"chartType":"bubble","title":"transparency=30","transparency":"30", + "legend":"bottom","data":D2}) +ch(BR,{"chartType":"bubble","title":"per-series gradients", + "gradients":"FF0000-0000FF;00FF00-FFFF00","legend":"bottom","data":D2}) + +new_slide("Presets & per-series Set") +for box,p in zip([TL,TR,BL],["minimal","dark","corporate"]): + ch(box,{"chartType":"bubble","preset":p,"title":f"preset={p}","legend":"bottom","data":D2}) +ch(BR,{"chartType":"bubble","title":"chart-series Set name+color","legend":"bottom","data":D2}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[1]", + *P({"name":"Renamed A","color":"C00000"})) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[2]", + *P({"name":"Renamed B","color":"2E75B6"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-column.md b/examples/ppt/charts/charts-column.md new file mode 100644 index 000000000..26dfff660 --- /dev/null +++ b/examples/ppt/charts/charts-column.md @@ -0,0 +1,67 @@ +# Column Charts Showcase — column, stackedColumn, percentStackedColumn, column3d + +Three files work together: + +- **charts-column.py** — build script (`python3 charts-column.py`). +- **charts-column.pptx** — generated deck. +- **charts-column.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-column.py +# → charts-column.pptx +``` + +## Slide map + +``` + Slide 1 Basic variants column / stackedColumn / percentStackedColumn / column3d + Slide 2 Title & legend title.font/size/color/bold, legend positions, legendFont + Slide 3 Data labels dataLabels flags, labelPos, labelfont + Slide 4 Axes axismin/max, axistitle, axisfont, axisline, axisnumfmt, + gridlines, minorGridlines, majorunit, minorunit, labelrotation, + dispunits, logbase, secondaryaxis, chart-axis Set + Slide 5 Series styling colors, gradient, gradients, transparency, seriesoutline, + seriesshadow, invertifneg, colorrule + Slide 6 Layout & overlays gapwidth, overlap, referenceline, errbars, trendline, dataTable + Slide 7 Backgrounds chartareafill, plotFill, chartborder, plotborder, roundedcorners + Slide 8 Presets & per-ser preset bundles + seriesN.name/values/color + chart-series Set +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=column \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-column.pptx b/examples/ppt/charts/charts-column.pptx new file mode 100644 index 000000000..d309fa578 Binary files /dev/null and b/examples/ppt/charts/charts-column.pptx differ diff --git a/examples/ppt/charts/charts-column.py b/examples/ppt/charts/charts-column.py new file mode 100644 index 000000000..353618251 --- /dev/null +++ b/examples/ppt/charts/charts-column.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +""" +Column Charts Showcase — column, stackedColumn, percentStackedColumn, column3d. + +Generates: charts-column.pptx + +Every column-applicable property officecli exposes is demonstrated at least +once across the slides: + + Slide 1 Basic variants column / stackedColumn / percentStackedColumn / column3d + Slide 2 Title & legend title.font/size/color/bold, legend positions, legendFont + Slide 3 Data labels dataLabels flags, labelPos, labelfont + Slide 4 Axes axismin/max, axistitle, axisfont, axisline, axisnumfmt, + gridlines, minorGridlines, majorunit, minorunit, labelrotation, + dispunits, logbase, secondaryaxis, chart-axis Set + Slide 5 Series styling colors, gradient, gradients, transparency, seriesoutline, + seriesshadow, invertifneg, colorrule + Slide 6 Layout & overlays gapwidth, overlap, referenceline, errbars, trendline, dataTable + Slide 7 Backgrounds chartareafill, plotFill, chartborder, plotborder, roundedcorners + Slide 8 Presets & per-ser preset bundles + seriesN.name/values/color + chart-series Set + +Usage: + python3 charts-column.py +""" +import subprocess, os, sys, atexit + +FILE = os.path.join(os.path.dirname(__file__), "charts-column.pptx") + +def cli(*args): + r = subprocess.run(["officecli", *args], capture_output=True, text=True) + if r.returncode != 0: + msg = (r.stderr or r.stdout or "").strip() + head = msg.splitlines()[0][:160] if msg else "" + if "UNSUPPORTED" in msg: + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(args[:3])} → {head}", file=sys.stderr) + return + if msg: + print(f" ! {' '.join(args[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) + +def props(d): + out = [] + for k, v in d.items(): + out += ["--prop", f"{k}={v}"] + return out + +slide = 0 +def new_slide(title): + global slide + slide += 1 + cli("add", FILE, "/", "--type", "slide") + cli("add", FILE, f"/slide[{slide}]", "--type", "shape", + *props({"text": title, "size": 24, "bold": "true", + "autoFit": "normal", "x": "0.5in", "y": "0.3in", "width": "12.3in", "height": "0.6in"})) + +def add_chart(box, p): + p = {**box, **p} + cli("add", FILE, f"/slide[{slide}]", "--type", "chart", *props(p)) + +# 2x2 grid boxes (widescreen 13.33 x 7.5in) +TL = {"x": "0.3in", "y": "1.05in", "width": "6.1in", "height": "3in"} +TR = {"x": "6.95in","y": "1.05in", "width": "6.1in", "height": "3in"} +BL = {"x": "0.3in", "y": "4.25in", "width": "6.1in", "height": "3in"} +BR = {"x": "6.95in","y": "4.25in", "width": "6.1in", "height": "3in"} +CATS = "Q1,Q2,Q3,Q4" +TWO_SERIES = "East:120,135,148,162;West:95,108,115,128" +THREE_SERIES = "East:120,135,148,162;South:95,108,115,128;West:80,90,98,110" + +if os.path.exists(FILE): + os.remove(FILE) +cli("create", FILE); cli("open", FILE) +atexit.register(lambda: (cli("close", FILE), cli("validate", FILE))) + +# --------------------------------------------------------------------------- +# Slide 1 — Basic variants +# --------------------------------------------------------------------------- +new_slide("Column variants — column / stackedColumn / percentStackedColumn / column3d") +add_chart(TL, {"chartType": "column", "title": "column", "legend": "bottom", + "categories": CATS, "data": TWO_SERIES}) +add_chart(TR, {"chartType": "stackedColumn", "title": "stackedColumn", "legend": "bottom", + "categories": CATS, "data": THREE_SERIES}) +add_chart(BL, {"chartType": "percentStackedColumn", "title": "percentStackedColumn", + "legend": "bottom", "categories": CATS, "data": THREE_SERIES}) +add_chart(BR, {"chartType": "column3d", "view3d": "15,20,30", "gapdepth": "150", + "title": "column3d (view3d=15,20,30)", "legend": "bottom", + "categories": CATS, "data": TWO_SERIES}) + +# --------------------------------------------------------------------------- +# Slide 2 — Title & legend +# --------------------------------------------------------------------------- +new_slide("Title & legend — title.font/size/color/bold, legend positions, legendFont") +add_chart(TL, {"chartType": "column", "title": "Styled title", + "title.font": "Georgia", "title.size": "20", "title.color": "4472C4", + "title.bold": "true", "legend": "bottom", + "categories": CATS, "data": TWO_SERIES}) +add_chart(TR, {"chartType": "column", "title": "legend=top + legendFont", + "legend": "top", "legendFont": "10:333333:Calibri", + "categories": CATS, "data": TWO_SERIES}) +add_chart(BL, {"chartType": "column", "title": "legend=topRight overlay", + "legend": "topRight", "legend.overlay": "true", + "categories": CATS, "data": TWO_SERIES}) +add_chart(BR, {"chartType": "column", "autotitledeleted": "true", "legend": "none", + "categories": CATS, "data": TWO_SERIES}) + +# --------------------------------------------------------------------------- +# Slide 3 — Data labels +# --------------------------------------------------------------------------- +new_slide("Data labels — flags (value/category/percent/none), labelPos, labelfont") +add_chart(TL, {"chartType": "column", "title": "value @ outsideEnd", + "dataLabels": "value", "labelPos": "outsideEnd", + "labelfont": "10:333333:Calibri", "legend": "none", + "categories": CATS, "data": "A:60,90,140,180"}) +add_chart(TR, {"chartType": "column", "title": "value,category @ insideEnd", + "dataLabels": "value,category", "labelPos": "insideEnd", + "labelfont": "9:FFFFFF:Calibri", "legend": "none", + "categories": CATS, "data": "A:60,90,140,180"}) +add_chart(BL, {"chartType": "stackedColumn", "title": "stacked + center labels", + "dataLabels": "value", "labelPos": "center", + "labelfont": "9:FFFFFF:Calibri", "legend": "bottom", + "categories": CATS, "data": THREE_SERIES}) +add_chart(BR, {"chartType": "column", "title": "dataLabels=none", + "dataLabels": "none", "legend": "none", + "categories": CATS, "data": "A:60,90,140,180"}) + +# --------------------------------------------------------------------------- +# Slide 4 — Axes +# --------------------------------------------------------------------------- +new_slide("Axes — min/max, titles, fonts, gridlines, units, log, secondary") +add_chart(TL, {"chartType": "column", "title": "axis min/max + titles + numfmt", + "legend": "none", + "axismin": "0", "axismax": "200", "majorunit": "50", "minorunit": "10", + "axistitle": "Revenue (USD)", "cattitle": "Quarter", + "axisfont": "10:333333:Calibri", "axisline": "666666:1", + "axisnumfmt": "#,##0", + "categories": CATS, "data": "Rev:60,90,140,180"}) +add_chart(TR, {"chartType": "column", "title": "gridlines + minorGridlines + ticks", + "legend": "none", + "gridlines": "E0E0E0:0.3", "minorGridlines": "F0F0F0:0.25", + "majorTickMark": "out", "minorTickMark": "in", "tickLabelPos": "nextTo", + "labelrotation": "-30", + "categories": "January,February,March,April", + "data": "A:60,90,140,180"}) +add_chart(BL, {"chartType": "column", "title": "dispunits=thousands", + "legend": "none", "dispunits": "thousands", + "categories": CATS, "data": "Rev:120000,135000,148000,162000"}) +add_chart(BR, {"chartType": "combo", "combotypes": "column,line", "secondaryaxis": "2", + "title": "secondaryaxis=2 (line on right)", "legend": "bottom", + "categories": CATS, "data": "Sales:120,135,148,162;Growth %:5,12,18,22"}) + +# Post-Add chart-axis Set on first chart +cli("set", FILE, f"/slide[{slide}]/chart[1]/axis[@role=value]", + *props({"title": "Revenue (USD)", "format": "$#,##0", + "majorGridlines": "true", "minorGridlines": "false", + "max": "200", "min": "0", "majorUnit": "50"})) + +# --------------------------------------------------------------------------- +# Slide 5 — Series styling +# --------------------------------------------------------------------------- +new_slide("Series styling — colors, gradient(s), transparency, outline, shadow, invertifneg, colorrule") +add_chart(TL, {"chartType": "column", "title": "colors + seriesoutline", + "legend": "bottom", + "colors": "4472C4,ED7D31,A5A5A5", + "seriesoutline": "000000:0.5", + "categories": CATS, "data": THREE_SERIES}) +add_chart(TR, {"chartType": "column", "title": "gradient + seriesshadow", + "legend": "bottom", + "gradient": "FF6600-FFCC00:90", + "seriesshadow": "000000-5-45-3-50", + "categories": CATS, "data": "A:60,90,140,180"}) +add_chart(BL, {"chartType": "column", "title": "per-series gradients + transparency=30", + "legend": "bottom", + "gradients": "FF0000-0000FF;00FF00-FFFF00", + "transparency": "30", + "categories": CATS, + "data": "A:60,90,140,180;B:40,70,100,130"}) +add_chart(BR, {"chartType": "column", "title": "invertifneg + colorrule", + "legend": "none", + "invertifneg": "true", + "colorrule": "0:FF0000:00AA00", + "categories": "Q1,Q2,Q3,Q4,Q5", + "data": "Net:60,-30,40,-50,80"}) + +# Recolor series 1 of the first chart via chart-series Set +cli("set", FILE, f"/slide[{slide}]/chart[1]/series[1]", *props({"color": "2E75B6"})) + +# --------------------------------------------------------------------------- +# Slide 6 — Layout & overlays +# --------------------------------------------------------------------------- +new_slide("Layout & overlays — gapwidth, overlap, referenceline, errbars, trendline, dataTable") +add_chart(TL, {"chartType": "column", "title": "gapwidth=50 + overlap=20", + "legend": "bottom", "gapwidth": "50", "overlap": "20", + "categories": CATS, "data": "A:60,90,140,180;B:50,75,110,150"}) +add_chart(TR, {"chartType": "column", "title": "referenceline=100", + "legend": "none", "referenceline": "100:FF0000:Target", + "categories": CATS, "data": "A:60,90,140,180"}) +add_chart(BL, {"chartType": "column", "title": "errbars=percentage:10", + "legend": "none", "errbars": "percentage:10", + "categories": CATS, "data": "A:60,90,140,180"}) +add_chart(BR, {"chartType": "column", "title": "dataTable=true + trendline=linear", + "legend": "bottom", "dataTable": "true", "trendline": "linear", + "categories": CATS, "data": "A:60,90,140,180"}) + +# --------------------------------------------------------------------------- +# Slide 7 — Backgrounds +# --------------------------------------------------------------------------- +new_slide("Backgrounds — chartareafill, plotFill, borders, roundedcorners") +add_chart(TL, {"chartType": "column", "title": "chartareafill + plotFill + borders", + "legend": "bottom", + "chartareafill": "FFF8E7", "plotFill": "FAFAFA", + "chartborder": "000000:1", "plotborder": "CCCCCC:0.5", + "categories": CATS, "data": "A:60,90,140,180"}) +add_chart(TR, {"chartType": "column", "title": "roundedcorners=true", + "legend": "bottom", + "roundedcorners": "true", "chartborder": "4472C4:2", + "categories": CATS, "data": "A:60,90,140,180"}) +add_chart(BL, {"chartType": "column", "title": "plotFill=none, gridlines=none", + "legend": "none", + "plotFill": "none", "gridlines": "none", + "categories": CATS, "data": "A:60,90,140,180"}) +add_chart(BR, {"chartType": "column", "title": "varyColors=true (single series)", + "legend": "none", "varyColors": "true", + "categories": CATS, "data": "A:60,90,140,180"}) + +# --------------------------------------------------------------------------- +# Slide 8 — Presets & per-series control +# --------------------------------------------------------------------------- +new_slide("Presets & per-series — preset bundles + seriesN.* + chart-series Set") +add_chart(TL, {"chartType": "column", "preset": "minimal", "title": "preset=minimal", + "legend": "bottom", "categories": CATS, + "data": "A:60,90,140,180;B:50,75,110,150"}) +add_chart(TR, {"chartType": "column", "preset": "corporate", "title": "preset=corporate", + "legend": "bottom", "categories": CATS, + "data": "A:60,90,140,180;B:50,75,110,150"}) +add_chart(BL, {"chartType": "column", "preset": "dark", "title": "preset=dark", + "legend": "bottom", "categories": CATS, + "data": "A:60,90,140,180;B:50,75,110,150"}) +add_chart(BR, {"chartType": "column", "title": "seriesN.* Add + chart-series Set", + "legend": "bottom", "categories": CATS, + "series1.name": "Product A", "series1.values": "60,90,140,180", + "series1.color": "4472C4", + "series2.name": "Product B", "series2.values": "50,75,110,150", + "series2.color": "ED7D31", + "series3.name": "Product C", "series3.values": "40,65,90,120", + "series3.color": "70AD47"}) +cli("set", FILE, f"/slide[{slide}]/chart[4]/series[1]", + *props({"name": "Renamed Alpha", "color": "C00000"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-combo.md b/examples/ppt/charts/charts-combo.md new file mode 100644 index 000000000..8529fa39b --- /dev/null +++ b/examples/ppt/charts/charts-combo.md @@ -0,0 +1,64 @@ +# Combo Charts Showcase — combotypes, combosplit, secondaryaxis + +Three files work together: + +- **charts-combo.py** — build script (`python3 charts-combo.py`). +- **charts-combo.pptx** — generated deck. +- **charts-combo.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-combo.py +# → charts-combo.pptx +``` + +## Slide map + +``` + Slide 1 combotypes mixes column+line, column+area, line+area, bar+line + Slide 2 combosplit split index 1, 2, 3 (first N series use primary) + Slide 3 secondaryaxis 1 series, 2 series, multiple series on secondary + Slide 4 Title & legend + Slide 5 Data labels + Slide 6 Axes min/max on both axes, titles, gridlines + Slide 7 Series styling colors, gradients, transparency, outline, shadow + Slide 8 Presets & per-series preset bundles + chart-series Set +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=combo \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-combo.pptx b/examples/ppt/charts/charts-combo.pptx new file mode 100644 index 000000000..9a67cc8c8 Binary files /dev/null and b/examples/ppt/charts/charts-combo.pptx differ diff --git a/examples/ppt/charts/charts-combo.py b/examples/ppt/charts/charts-combo.py new file mode 100644 index 000000000..de2c77356 --- /dev/null +++ b/examples/ppt/charts/charts-combo.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Combo Charts Showcase — combotypes, combosplit, secondaryaxis. + +Generates: charts-combo.pptx + + Slide 1 combotypes mixes column+line, column+area, line+area, bar+line + Slide 2 combosplit split index 1, 2, 3 (first N series use primary) + Slide 3 secondaryaxis 1 series, 2 series, multiple series on secondary + Slide 4 Title & legend + Slide 5 Data labels + Slide 6 Axes min/max on both axes, titles, gridlines + Slide 7 Series styling colors, gradients, transparency, outline, shadow + Slide 8 Presets & per-series preset bundles + chart-series Set + +Usage: + python3 charts-combo.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-combo.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +CATS="Q1,Q2,Q3,Q4" +D2="Sales:120,135,148,162;Growth %:5,12,18,22" +D3="Sales:120,135,148,162;Cost:80,90,95,105;Growth %:5,12,18,22" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("combotypes — column+line / column+area / line+area / bar+line") +ch(TL,{"chartType":"combo","combotypes":"column,line","title":"column + line", + "legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"combo","combotypes":"column,area","title":"column + area", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"combo","combotypes":"line,area","title":"line + area", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"combo","combotypes":"bar,line","title":"bar + line", + "legend":"bottom","categories":CATS,"data":D2}) + +new_slide("combosplit — first N series use primary type") +ch(TL,{"chartType":"combo","combotypes":"column,column,line","combosplit":"2", + "title":"combosplit=2 (2 columns + 1 line)","legend":"bottom", + "categories":CATS,"data":D3}) +ch(TR,{"chartType":"combo","combotypes":"column,line,line","combosplit":"1", + "title":"combosplit=1 (1 column + 2 lines)","legend":"bottom", + "categories":CATS,"data":D3}) +ch(BL,{"chartType":"combo","combotypes":"line,line,column","combosplit":"2", + "title":"combosplit=2 (2 lines + 1 column)","legend":"bottom", + "categories":CATS,"data":D3}) +ch(BR,{"chartType":"combo","combotypes":"area,column,line","combosplit":"1", + "title":"area + column + line","legend":"bottom", + "categories":CATS,"data":D3}) + +new_slide("secondaryaxis — line on secondary value axis") +ch(TL,{"chartType":"combo","combotypes":"column,line","secondaryaxis":"2", + "title":"secondaryaxis=2","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"combo","combotypes":"column,column,line","secondaryaxis":"3","combosplit":"2", + "title":"secondaryaxis=3 (Growth on right)","legend":"bottom", + "categories":CATS,"data":D3}) +ch(BL,{"chartType":"combo","combotypes":"column,line,line","secondaryaxis":"2,3","combosplit":"1", + "title":"secondaryaxis=2,3","legend":"bottom","categories":CATS,"data":D3}) +ch(BR,{"chartType":"combo","combotypes":"column,line","secondaryaxis":"2", + "title":"with grid + tick fonts","secondaryaxis":"2", + "gridlines":"E0E0E0:0.3","axisfont":"9:333333:Calibri", + "legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Title & legend") +ch(TL,{"chartType":"combo","combotypes":"column,line","title":"Styled title", + "title.font":"Georgia","title.size":"20","title.color":"4472C4","title.bold":"true", + "legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"combo","combotypes":"column,line","title":"legend=top + legendFont", + "legend":"top","legendFont":"10:333333:Calibri","categories":CATS,"data":D2}) +ch(BL,{"chartType":"combo","combotypes":"column,line","title":"legend.overlay=true", + "legend":"topRight","legend.overlay":"true","categories":CATS,"data":D2}) +ch(BR,{"chartType":"combo","combotypes":"column,line","autotitledeleted":"true", + "legend":"none","categories":CATS,"data":D2}) + +new_slide("Data labels — combo charts skip labelPos (chart-type conditional)") +ch(TL,{"chartType":"combo","combotypes":"column,line","title":"dataLabels=value", + "dataLabels":"value","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"combo","combotypes":"column,line","title":"value,series", + "dataLabels":"value,series","legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"combo","combotypes":"column,line","title":"dataLabels=none", + "dataLabels":"none","legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"combo","combotypes":"column,line","title":"labelfont styled", + "dataLabels":"value","labelfont":"10:C00000:Georgia", + "legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Axes — min/max on primary, secondary, gridlines, axisnumfmt") +ch(TL,{"chartType":"combo","combotypes":"column,line","secondaryaxis":"2", + "title":"both axes min/max","axismin":"0","axismax":"200", + "axistitle":"Sales","cattitle":"Quarter","axisfont":"10:333333:Calibri", + "axisnumfmt":"#,##0","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"combo","combotypes":"column,line","title":"gridlines + minorGridlines", + "gridlines":"E0E0E0:0.3","minorGridlines":"F0F0F0:0.25", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"combo","combotypes":"column,line","title":"labelrotation=-30", + "labelrotation":"-30","legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"combo","combotypes":"column,line","title":"chart-axis Set after add", + "legend":"bottom","categories":CATS,"data":D2}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/axis[@role=value]", + *P({"title":"Sales (USD)","format":"$#,##0","majorGridlines":"true","min":"0","max":"200"})) + +new_slide("Series styling — colors, gradient(s), transparency, outline, shadow") +ch(TL,{"chartType":"combo","combotypes":"column,line","title":"colors + seriesoutline", + "colors":"4472C4,ED7D31","seriesoutline":"000000:0.5", + "legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"combo","combotypes":"column,line","title":"gradient + seriesshadow", + "gradient":"FF6600-FFCC00","seriesshadow":"000000-5-45-3-50", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"combo","combotypes":"column,line","title":"transparency=30", + "transparency":"30","legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"combo","combotypes":"column,line","title":"per-series gradients", + "gradients":"FF0000-0000FF;00FF00-FFFF00", + "legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Presets & per-series Set") +for box,p in zip([TL,TR,BL],["minimal","dark","corporate"]): + ch(box,{"chartType":"combo","combotypes":"column,line","preset":p, + "title":f"preset={p}","legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"combo","combotypes":"column,line","title":"chart-series Set", + "legend":"bottom","categories":CATS,"data":D2}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[1]", + *P({"name":"Renamed Sales","color":"C00000"})) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[2]", + *P({"name":"Renamed Growth","color":"2E75B6","lineWidth":"2.5","marker":"circle","markerSize":"8"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-doughnut.md b/examples/ppt/charts/charts-doughnut.md new file mode 100644 index 000000000..76796efa1 --- /dev/null +++ b/examples/ppt/charts/charts-doughnut.md @@ -0,0 +1,64 @@ +# Doughnut Charts Showcase + +Three files work together: + +- **charts-doughnut.py** — build script (`python3 charts-doughnut.py`). +- **charts-doughnut.pptx** — generated deck. +- **charts-doughnut.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-doughnut.py +# → charts-doughnut.pptx +``` + +## Slide map + +``` + Slide 1 holeSize variants holeSize=10/30/55/75 + Slide 2 Multi-ring two-series + three-series concentric rings + Slide 3 firstSliceAngle 0 / 90 / 180 / 270 + Slide 4 Data labels percent / category / value, leaderlines, labelfont + Slide 5 Series styling colors, gradient, seriesoutline, seriesshadow, transparency + Slide 6 Title & legend title.* + legend positions + legendFont + Slide 7 Backgrounds chartareafill, plotFill, chartborder, roundedcorners + Slide 8 Presets & per-series preset bundles + chart-series Set +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=doughnut \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-doughnut.pptx b/examples/ppt/charts/charts-doughnut.pptx new file mode 100644 index 000000000..04096ede4 Binary files /dev/null and b/examples/ppt/charts/charts-doughnut.pptx differ diff --git a/examples/ppt/charts/charts-doughnut.py b/examples/ppt/charts/charts-doughnut.py new file mode 100644 index 000000000..10bf9738b --- /dev/null +++ b/examples/ppt/charts/charts-doughnut.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +Doughnut Charts Showcase. + +Generates: charts-doughnut.pptx + + Slide 1 holeSize variants holeSize=10/30/55/75 + Slide 2 Multi-ring two-series + three-series concentric rings + Slide 3 firstSliceAngle 0 / 90 / 180 / 270 + Slide 4 Data labels percent / category / value, leaderlines, labelfont + Slide 5 Series styling colors, gradient, seriesoutline, seriesshadow, transparency + Slide 6 Title & legend title.* + legend positions + legendFont + Slide 7 Backgrounds chartareafill, plotFill, chartborder, roundedcorners + Slide 8 Presets & per-series preset bundles + chart-series Set + +Usage: + python3 charts-doughnut.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-doughnut.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +CATS="North,South,East,West"; D="Share:30,25,28,17" +D2="Last:25,30,25,20;This:30,25,28,17" +D3="Region1:30,25,28,17;Region2:25,30,20,25;Region3:20,25,30,25" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("holeSize — 10 / 30 / 55 / 75") +for box,h in zip([TL,TR,BL,BR],[10,30,55,75]): + ch(box,{"chartType":"doughnut","title":f"holeSize={h}","holeSize":str(h), + "legend":"right","varyColors":"true","categories":CATS,"data":D}) + +new_slide("Multi-ring — concentric series") +ch(TL,{"chartType":"doughnut","title":"single ring","holeSize":"50","legend":"right", + "categories":CATS,"data":D}) +ch(TR,{"chartType":"doughnut","title":"two rings","holeSize":"40","legend":"right", + "categories":CATS,"data":D2}) +ch(BL,{"chartType":"doughnut","title":"three rings","holeSize":"30","legend":"right", + "categories":CATS,"data":D3}) +ch(BR,{"chartType":"doughnut","title":"two rings + dataLabels=percent","holeSize":"40", + "dataLabels":"percent","legend":"right","categories":CATS,"data":D2}) + +new_slide("First slice angle — 0 / 90 / 180 / 270") +for box,ang in zip([TL,TR,BL,BR],[0,90,180,270]): + ch(box,{"chartType":"doughnut","title":f"firstSliceAngle={ang}", + "firstSliceAngle":str(ang),"holeSize":"50","legend":"right", + "varyColors":"true","categories":CATS,"data":D}) + +new_slide("Data labels — percent / category / value, leaderlines, labelfont") +ch(TL,{"chartType":"doughnut","title":"dataLabels=percent","dataLabels":"percent","holeSize":"50", + "legend":"right","labelfont":"10:333333:Calibri","categories":CATS,"data":D}) +ch(TR,{"chartType":"doughnut","title":"percent,category","dataLabels":"percent,category", + "holeSize":"50","leaderlines":"true","legend":"none", + "labelfont":"10:333333:Calibri","categories":CATS,"data":D}) +ch(BL,{"chartType":"doughnut","title":"all flags","dataLabels":"value,percent,category", + "holeSize":"50","leaderlines":"true","legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"doughnut","title":"dataLabels=none","dataLabels":"none","holeSize":"50", + "legend":"right","categories":CATS,"data":D}) + +new_slide("Series styling — colors, gradient, outline, shadow, transparency") +ch(TL,{"chartType":"doughnut","title":"colors=","holeSize":"50","legend":"right", + "colors":"4472C4,ED7D31,A5A5A5,70AD47","categories":CATS,"data":D}) +ch(TR,{"chartType":"doughnut","title":"gradient + seriesshadow","holeSize":"50", + "gradient":"FF6600-FFCC00","seriesshadow":"000000-5-45-3-50", + "legend":"right","categories":CATS,"data":D}) +ch(BL,{"chartType":"doughnut","title":"seriesoutline white","holeSize":"50", + "seriesoutline":"FFFFFF:2","legend":"right","categories":CATS,"data":D}) +ch(BR,{"chartType":"doughnut","title":"transparency=30","holeSize":"50", + "transparency":"30","legend":"right","categories":CATS,"data":D}) + +new_slide("Title & legend") +ch(TL,{"chartType":"doughnut","title":"Styled title","title.font":"Georgia","title.size":"20", + "title.color":"4472C4","title.bold":"true","holeSize":"50","legend":"right", + "categories":CATS,"data":D}) +ch(TR,{"chartType":"doughnut","title":"legend=bottom + legendFont","holeSize":"50", + "legend":"bottom","legendFont":"10:333333:Calibri","categories":CATS,"data":D}) +ch(BL,{"chartType":"doughnut","title":"legend.overlay=true","holeSize":"50", + "legend":"topRight","legend.overlay":"true","categories":CATS,"data":D}) +ch(BR,{"chartType":"doughnut","autotitledeleted":"true","holeSize":"50","legend":"none", + "categories":CATS,"data":D}) + +new_slide("Backgrounds — chartareafill, plotFill, chartborder, roundedcorners") +ch(TL,{"chartType":"doughnut","title":"chartareafill + chartborder","holeSize":"50", + "chartareafill":"FFF8E7","chartborder":"000000:1","legend":"right", + "categories":CATS,"data":D}) +ch(TR,{"chartType":"doughnut","title":"roundedcorners=true","holeSize":"50", + "roundedcorners":"true","chartborder":"4472C4:2","legend":"right", + "categories":CATS,"data":D}) +ch(BL,{"chartType":"doughnut","title":"plotFill=none","holeSize":"50", + "plotFill":"none","legend":"right","categories":CATS,"data":D}) +ch(BR,{"chartType":"doughnut","title":"chartareafill=none","holeSize":"50", + "chartareafill":"none","legend":"right","categories":CATS,"data":D}) + +new_slide("Presets & per-series Set") +for box,p in zip([TL,TR,BL],["minimal","dark","corporate"]): + ch(box,{"chartType":"doughnut","preset":p,"title":f"preset={p}","holeSize":"50", + "legend":"right","categories":CATS,"data":D}) +ch(BR,{"chartType":"doughnut","title":"chart-series Set name+color","holeSize":"50", + "legend":"right","categories":CATS,"data":D}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[1]", + *P({"name":"Renamed Share","color":"C00000"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-line.md b/examples/ppt/charts/charts-line.md new file mode 100644 index 000000000..14fa2e7cc --- /dev/null +++ b/examples/ppt/charts/charts-line.md @@ -0,0 +1,64 @@ +# Line Charts Showcase — line, stackedLine, percentStackedLine, line3d + +Three files work together: + +- **charts-line.py** — build script (`python3 charts-line.py`). +- **charts-line.pptx** — generated deck. +- **charts-line.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-line.py +# → charts-line.pptx +``` + +## Slide map + +``` + Slide 1 Variants line / stackedLine / percentStackedLine / line3d + Slide 2 Markers marker symbol/size/color, markersize, showMarker + Slide 3 Smoothing & dash smooth, linedash, linewidth + Slide 4 Title & legend title.* + legend positions + legendFont + Slide 5 Data labels flags, labelPos, labelfont + Slide 6 Axes min/max, titles, fonts, gridlines, ticks, labelrotation, log + Slide 7 Overlays droplines, hilowlines, updownbars, trendline, errbars, referenceline + Slide 8 Per-series Set lineWidth/lineDash/marker/markerSize/color/smooth + presets +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=line \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-line.pptx b/examples/ppt/charts/charts-line.pptx new file mode 100644 index 000000000..eaa0fc2cd Binary files /dev/null and b/examples/ppt/charts/charts-line.pptx differ diff --git a/examples/ppt/charts/charts-line.py b/examples/ppt/charts/charts-line.py new file mode 100644 index 000000000..9de250952 --- /dev/null +++ b/examples/ppt/charts/charts-line.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Line Charts Showcase — line, stackedLine, percentStackedLine, line3d. + +Generates: charts-line.pptx + + Slide 1 Variants line / stackedLine / percentStackedLine / line3d + Slide 2 Markers marker symbol/size/color, markersize, showMarker + Slide 3 Smoothing & dash smooth, linedash, linewidth + Slide 4 Title & legend title.* + legend positions + legendFont + Slide 5 Data labels flags, labelPos, labelfont + Slide 6 Axes min/max, titles, fonts, gridlines, ticks, labelrotation, log + Slide 7 Overlays droplines, hilowlines, updownbars, trendline, errbars, referenceline + Slide 8 Per-series Set lineWidth/lineDash/marker/markerSize/color/smooth + presets + +Usage: + python3 charts-line.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-line.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m = (r.stderr or r.stdout or "").strip().splitlines() + head = m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +CATS="Mon,Tue,Wed,Thu,Fri"; D2="A:50,60,70,65,80;B:40,45,55,60,75" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("Line variants — line / stackedLine / percentStackedLine / line3d") +ch(TL,{"chartType":"line","title":"line","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"stackedLine","title":"stackedLine","legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"percentStackedLine","title":"percentStackedLine","legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"line3d","title":"line3d","legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Markers — symbol, size, color, showMarker") +ch(TL,{"chartType":"line","title":"marker=circle:8:FF0000","marker":"circle:8:FF0000", + "linewidth":"2","legend":"none","categories":CATS,"data":"A:50,60,70,65,80"}) +ch(TR,{"chartType":"line","title":"marker=square:6","marker":"square:6","linewidth":"2", + "legend":"none","categories":CATS,"data":"A:50,60,70,65,80"}) +ch(BL,{"chartType":"line","title":"marker=diamond:10:0070C0","marker":"diamond:10:0070C0", + "linewidth":"2","legend":"none","categories":CATS,"data":"A:50,60,70,65,80"}) +ch(BR,{"chartType":"line","title":"showMarker=true (default markers)","showMarker":"true", + "legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Smoothing & dash — smooth, linedash, linewidth") +ch(TL,{"chartType":"line","title":"smooth=true","smooth":"true","linewidth":"2.5", + "legend":"none","categories":CATS,"data":"A:50,60,70,65,80"}) +ch(TR,{"chartType":"line","title":"linedash=dash","linedash":"dash","linewidth":"2", + "legend":"none","categories":CATS,"data":"A:50,60,70,65,80"}) +ch(BL,{"chartType":"line","title":"linedash=dot","linedash":"dot","linewidth":"2", + "legend":"none","categories":CATS,"data":"A:50,60,70,65,80"}) +ch(BR,{"chartType":"line","title":"linedash=dashDot","linedash":"dashDot","linewidth":"2", + "legend":"none","categories":CATS,"data":"A:50,60,70,65,80"}) + +new_slide("Title & legend") +ch(TL,{"chartType":"line","title":"Styled title","title.font":"Georgia","title.size":"20", + "title.color":"4472C4","title.bold":"true","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"line","title":"legend=top + legendFont","legend":"top", + "legendFont":"10:333333:Calibri","categories":CATS,"data":D2}) +ch(BL,{"chartType":"line","title":"legend.overlay=true","legend":"topRight", + "legend.overlay":"true","categories":CATS,"data":D2}) +ch(BR,{"chartType":"line","autotitledeleted":"true","legend":"none","categories":CATS,"data":D2}) + +new_slide("Data labels — flags, labelPos, labelfont") +ch(TL,{"chartType":"line","title":"dataLabels=value @ top","dataLabels":"value","labelPos":"top", + "labelfont":"10:333333:Calibri","legend":"none","categories":CATS,"data":"A:50,60,70,65,80"}) +ch(TR,{"chartType":"line","title":"value,category","dataLabels":"value,category","labelPos":"top", + "legend":"none","categories":CATS,"data":"A:50,60,70,65,80"}) +ch(BL,{"chartType":"line","title":"dataLabels=none","dataLabels":"none","legend":"none", + "categories":CATS,"data":"A:50,60,70,65,80"}) +ch(BR,{"chartType":"line","title":"labelfont styled","dataLabels":"value","labelPos":"top", + "labelfont":"12:C00000:Georgia","legend":"none","categories":CATS,"data":"A:50,60,70,65,80"}) + +new_slide("Axes — min/max, gridlines, ticks, labelrotation, log") +ch(TL,{"chartType":"line","title":"min/max + titles","legend":"none", + "axismin":"0","axismax":"100","majorunit":"25","axistitle":"Visits","cattitle":"Day", + "axisfont":"10:333333:Calibri","axisline":"666666:1","axisnumfmt":"#,##0", + "categories":CATS,"data":"A:50,60,70,65,80"}) +ch(TR,{"chartType":"line","title":"gridlines + ticks","legend":"none", + "gridlines":"E0E0E0:0.3","minorGridlines":"F0F0F0:0.25", + "majorTickMark":"out","minorTickMark":"in","tickLabelPos":"nextTo", + "categories":CATS,"data":"A:50,60,70,65,80"}) +ch(TR if False else BL,{"chartType":"line","title":"labelrotation=-30","legend":"none", + "labelrotation":"-30","categories":"January,February,March,April,May,June", + "data":"A:60,90,140,180,160,210"}) +ch(BR,{"chartType":"line","title":"logbase=10","legend":"none","logbase":"10", + "axismin":"1","axismax":"10000","categories":CATS,"data":"Growth:5,50,500,5000,3000"}) + +new_slide("Overlays — droplines, hilowlines, updownbars, trendline, errbars, referenceline") +ch(TL,{"chartType":"line","title":"droplines + hilowlines","droplines":"808080:0.5","hilowlines":"true", + "legend":"bottom","categories":CATS,"data":"High:130,135,140,138,145;Low:118,122,128,125,132"}) +ch(TR,{"chartType":"line","title":"updownbars=150:00AA00:FF0000", + "updownbars":"150:00AA00:FF0000","legend":"bottom","categories":CATS, + "data":"Open:120,128,130,135,138;Close:128,125,135,138,142"}) +ch(BL,{"chartType":"line","title":"trendline=linear + errbars=stdDev:1", + "trendline":"linear","errbars":"stdDev:1","legend":"none", + "categories":CATS,"data":"A:50,60,70,65,80"}) +ch(BR,{"chartType":"line","title":"referenceline=70:FF0000:Target", + "referenceline":"70:FF0000:Target","legend":"none", + "categories":CATS,"data":"A:50,60,70,65,80"}) + +new_slide("Per-series Set + presets — chart-series lineWidth/lineDash/marker/markerSize/color/smooth") +ch(TL,{"chartType":"line","preset":"minimal","title":"preset=minimal", + "legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"line","preset":"dark","title":"preset=dark", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"line","preset":"corporate","title":"preset=corporate", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"line","title":"chart-series Set per line","showMarker":"true", + "legend":"bottom","categories":CATS,"data":D2}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[1]", + *P({"name":"Alpha","color":"C00000","lineWidth":"2.5","lineDash":"solid", + "marker":"circle","markerSize":"9","smooth":"true"})) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[2]", + *P({"name":"Beta","color":"2E75B6","lineWidth":"1.5","lineDash":"dash", + "marker":"diamond","markerSize":"8"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-pie.md b/examples/ppt/charts/charts-pie.md new file mode 100644 index 000000000..da4479eb4 --- /dev/null +++ b/examples/ppt/charts/charts-pie.md @@ -0,0 +1,64 @@ +# Pie Charts Showcase — pie, pie3d, pieOfPie, barOfPie (where supported) + +Three files work together: + +- **charts-pie.py** — build script (`python3 charts-pie.py`). +- **charts-pie.pptx** — generated deck. +- **charts-pie.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-pie.py +# → charts-pie.pptx +``` + +## Slide map + +``` + Slide 1 Variants pie / pie3d (view3d) — varyColors, firstSliceAngle + Slide 2 Explosion explosion=0/10/20/30 + Slide 3 Title & legend title.* + legend positions + legendFont + Slide 4 Data labels flags (percent/category/value), labelfont, leaderlines + Slide 5 Series styling colors, gradient, transparency, seriesoutline, seriesshadow + Slide 6 First-slice angle 0 / 90 / 180 / 270 + Slide 7 Backgrounds chartareafill, plotFill, chartborder, roundedcorners + Slide 8 Presets & per-pt preset bundles + per-point recolor via chart-series Set +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=pie \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-pie.pptx b/examples/ppt/charts/charts-pie.pptx new file mode 100644 index 000000000..1f3142e43 Binary files /dev/null and b/examples/ppt/charts/charts-pie.pptx differ diff --git a/examples/ppt/charts/charts-pie.py b/examples/ppt/charts/charts-pie.py new file mode 100644 index 000000000..623c24708 --- /dev/null +++ b/examples/ppt/charts/charts-pie.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +Pie Charts Showcase — pie, pie3d, pieOfPie, barOfPie (where supported). + +Generates: charts-pie.pptx + + Slide 1 Variants pie / pie3d (view3d) — varyColors, firstSliceAngle + Slide 2 Explosion explosion=0/10/20/30 + Slide 3 Title & legend title.* + legend positions + legendFont + Slide 4 Data labels flags (percent/category/value), labelfont, leaderlines + Slide 5 Series styling colors, gradient, transparency, seriesoutline, seriesshadow + Slide 6 First-slice angle 0 / 90 / 180 / 270 + Slide 7 Backgrounds chartareafill, plotFill, chartborder, roundedcorners + Slide 8 Presets & per-pt preset bundles + per-point recolor via chart-series Set + +Usage: + python3 charts-pie.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-pie.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +CATS="North,South,East,West"; D="Share:30,25,28,17" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("Pie variants — pie / pie3d (varyColors, firstSliceAngle)") +ch(TL,{"chartType":"pie","title":"pie","legend":"right","varyColors":"true", + "categories":CATS,"data":D}) +ch(TR,{"chartType":"pie3d","title":"pie3d (view3d=20,20,30)","view3d":"20,20,30", + "legend":"right","varyColors":"true","categories":CATS,"data":D}) +ch(BL,{"chartType":"pie","title":"firstSliceAngle=90","firstSliceAngle":"90", + "legend":"right","categories":CATS,"data":D}) +ch(BR,{"chartType":"pie","title":"varyColors=false","varyColors":"false", + "legend":"right","categories":CATS,"data":D}) + +new_slide("Explosion — 0 / 10 / 20 / 30 (% of radius)") +ch(TL,{"chartType":"pie","title":"explosion=0","explosion":"0","legend":"right", + "categories":CATS,"data":D}) +ch(TR,{"chartType":"pie","title":"explosion=10","explosion":"10","legend":"right", + "categories":CATS,"data":D}) +ch(BL,{"chartType":"pie","title":"explosion=20","explosion":"20","legend":"right", + "categories":CATS,"data":D}) +ch(BR,{"chartType":"pie","title":"explosion=30","explosion":"30","legend":"right", + "categories":CATS,"data":D}) + +new_slide("Title & legend") +ch(TL,{"chartType":"pie","title":"Styled title","title.font":"Georgia","title.size":"20", + "title.color":"4472C4","title.bold":"true","legend":"right","categories":CATS,"data":D}) +ch(TR,{"chartType":"pie","title":"legend=bottom + legendFont","legend":"bottom", + "legendFont":"10:333333:Calibri","categories":CATS,"data":D}) +ch(BL,{"chartType":"pie","title":"legend.overlay=true","legend":"topRight", + "legend.overlay":"true","categories":CATS,"data":D}) +ch(BR,{"chartType":"pie","autotitledeleted":"true","legend":"none","categories":CATS,"data":D}) + +new_slide("Data labels — percent / category / value, labelfont, leaderlines") +ch(TL,{"chartType":"pie","title":"dataLabels=percent","dataLabels":"percent", + "legend":"right","labelfont":"10:333333:Calibri","categories":CATS,"data":D}) +ch(TR,{"chartType":"pie","title":"percent,category","dataLabels":"percent,category", + "leaderlines":"true","legend":"none","labelfont":"10:333333:Calibri", + "categories":CATS,"data":D}) +ch(BL,{"chartType":"pie","title":"all flags","dataLabels":"value,percent,category", + "leaderlines":"true","legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"pie","title":"dataLabels=none","dataLabels":"none","legend":"right", + "categories":CATS,"data":D}) + +new_slide("Series styling — colors, gradient, transparency, outline, shadow") +ch(TL,{"chartType":"pie","title":"colors= explicit palette","legend":"right", + "colors":"4472C4,ED7D31,A5A5A5,70AD47","categories":CATS,"data":D}) +ch(TR,{"chartType":"pie","title":"gradient + seriesshadow","legend":"right", + "gradient":"FF6600-FFCC00","seriesshadow":"000000-5-45-3-50", + "categories":CATS,"data":D}) +ch(BL,{"chartType":"pie","title":"seriesoutline white","legend":"right", + "seriesoutline":"FFFFFF:2","categories":CATS,"data":D}) +ch(BR,{"chartType":"pie","title":"transparency=30","legend":"right", + "transparency":"30","categories":CATS,"data":D}) + +new_slide("First slice angle — 0 / 90 / 180 / 270") +for box,ang in zip([TL,TR,BL,BR],[0,90,180,270]): + ch(box,{"chartType":"pie","title":f"firstSliceAngle={ang}", + "firstSliceAngle":str(ang),"legend":"right", + "varyColors":"true","categories":CATS,"data":D}) + +new_slide("Backgrounds — chartareafill, plotFill, chartborder, roundedcorners") +ch(TL,{"chartType":"pie","title":"chartareafill + chartborder","legend":"right", + "chartareafill":"FFF8E7","chartborder":"000000:1","categories":CATS,"data":D}) +ch(TR,{"chartType":"pie","title":"roundedcorners=true","legend":"right", + "roundedcorners":"true","chartborder":"4472C4:2","categories":CATS,"data":D}) +ch(BL,{"chartType":"pie","title":"plotFill=none","legend":"right", + "plotFill":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"pie","title":"chartareafill=none","legend":"right", + "chartareafill":"none","categories":CATS,"data":D}) + +new_slide("Presets & per-series Set") +for box,p in zip([TL,TR,BL],["minimal","dark","corporate"]): + ch(box,{"chartType":"pie","preset":p,"title":f"preset={p}","legend":"right", + "categories":CATS,"data":D}) +ch(BR,{"chartType":"pie","title":"chart-series Set name+color","legend":"right", + "categories":CATS,"data":D}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[1]", + *P({"name":"Renamed Share","color":"C00000"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-radar.md b/examples/ppt/charts/charts-radar.md new file mode 100644 index 000000000..7c6691a62 --- /dev/null +++ b/examples/ppt/charts/charts-radar.md @@ -0,0 +1,64 @@ +# Radar Charts Showcase — radarstyle standard / marker / filled + +Three files work together: + +- **charts-radar.py** — build script (`python3 charts-radar.py`). +- **charts-radar.pptx** — generated deck. +- **charts-radar.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-radar.py +# → charts-radar.pptx +``` + +## Slide map + +``` + Slide 1 radarstyle standard / marker / filled + Slide 2 Title & legend title.* + legend positions + legendFont + Slide 3 Data labels flags + labelfont + Slide 4 Axes min/max, gridlines, axisfont, labelrotation + Slide 5 Series styling colors, gradient, transparency, outline, shadow + Slide 6 Markers marker symbol/size/color (radarstyle=marker only) + Slide 7 Backgrounds chartareafill, plotFill, chartborder, roundedcorners + Slide 8 Presets & per-series preset bundles + chart-series Set +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=radar \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-radar.pptx b/examples/ppt/charts/charts-radar.pptx new file mode 100644 index 000000000..6f37745d7 Binary files /dev/null and b/examples/ppt/charts/charts-radar.pptx differ diff --git a/examples/ppt/charts/charts-radar.py b/examples/ppt/charts/charts-radar.py new file mode 100644 index 000000000..6e0f3a175 --- /dev/null +++ b/examples/ppt/charts/charts-radar.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +Radar Charts Showcase — radarstyle standard / marker / filled. + +Generates: charts-radar.pptx + + Slide 1 radarstyle standard / marker / filled + Slide 2 Title & legend title.* + legend positions + legendFont + Slide 3 Data labels flags + labelfont + Slide 4 Axes min/max, gridlines, axisfont, labelrotation + Slide 5 Series styling colors, gradient, transparency, outline, shadow + Slide 6 Markers marker symbol/size/color (radarstyle=marker only) + Slide 7 Backgrounds chartareafill, plotFill, chartborder, roundedcorners + Slide 8 Presets & per-series preset bundles + chart-series Set + +Usage: + python3 charts-radar.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-radar.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +CATS="Speed,Power,Range,Style,Tech,Price" +D="A:8,7,9,6,8,7" +D2="Model A:8,7,9,6,8,7;Model B:6,9,7,8,9,6" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("radarstyle — standard / marker / filled") +ch(TL,{"chartType":"radar","radarstyle":"standard","title":"radarstyle=standard", + "legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"radar","radarstyle":"marker","title":"radarstyle=marker", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"radar","radarstyle":"filled","title":"radarstyle=filled", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"radar","radarstyle":"standard","title":"single series", + "legend":"bottom","categories":CATS,"data":D}) + +new_slide("Title & legend") +ch(TL,{"chartType":"radar","radarstyle":"filled","title":"Styled title", + "title.font":"Georgia","title.size":"20","title.color":"4472C4","title.bold":"true", + "legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"radar","radarstyle":"standard","title":"legend=top + legendFont", + "legend":"top","legendFont":"10:333333:Calibri","categories":CATS,"data":D2}) +ch(BL,{"chartType":"radar","radarstyle":"standard","title":"legend.overlay=true", + "legend":"topRight","legend.overlay":"true","categories":CATS,"data":D2}) +ch(BR,{"chartType":"radar","radarstyle":"filled","autotitledeleted":"true","legend":"none", + "categories":CATS,"data":D2}) + +new_slide("Data labels — flags + labelfont") +ch(TL,{"chartType":"radar","radarstyle":"marker","title":"value","dataLabels":"value", + "labelfont":"9:333333:Calibri","legend":"none","categories":CATS,"data":D}) +ch(TR,{"chartType":"radar","radarstyle":"marker","title":"value,series", + "dataLabels":"value,series","legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"radar","radarstyle":"standard","title":"value,category", + "dataLabels":"value,category","legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"radar","radarstyle":"filled","title":"dataLabels=none", + "dataLabels":"none","legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Axes — min/max, gridlines, axisfont, labelrotation") +ch(TL,{"chartType":"radar","radarstyle":"standard","title":"min/max + titles", + "axismin":"0","axismax":"10","majorunit":"2","axisfont":"10:333333:Calibri", + "legend":"none","categories":CATS,"data":D}) +ch(TR,{"chartType":"radar","radarstyle":"standard","title":"gridlines + minorGridlines", + "gridlines":"E0E0E0:0.3","minorGridlines":"F0F0F0:0.25", + "legend":"none","categories":CATS,"data":D}) +ch(BL,{"chartType":"radar","radarstyle":"standard","title":"labelrotation=30", + "labelrotation":"30","legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"radar","radarstyle":"standard","title":"axisnumfmt=0.0", + "axisnumfmt":"0.0","legend":"none","categories":CATS,"data":D}) + +new_slide("Series styling — colors, gradient, transparency, outline, shadow") +ch(TL,{"chartType":"radar","radarstyle":"filled","title":"colors + seriesoutline", + "colors":"4472C4,ED7D31","seriesoutline":"000000:0.5", + "legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"radar","radarstyle":"filled","title":"gradient + seriesshadow", + "gradient":"FF6600-FFCC00","seriesshadow":"000000-5-45-3-50", + "legend":"none","categories":CATS,"data":D}) +ch(BL,{"chartType":"radar","radarstyle":"filled","title":"transparency=40", + "transparency":"40","legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"radar","radarstyle":"filled","title":"per-series gradients", + "gradients":"FF0000-0000FF;00FF00-FFFF00","legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Markers (radarstyle=marker) — symbol/size/color") +ch(TL,{"chartType":"radar","radarstyle":"marker","title":"circle:10:FF0000", + "marker":"circle:10:FF0000","legend":"none","categories":CATS,"data":D}) +ch(TR,{"chartType":"radar","radarstyle":"marker","title":"square:8:0070C0", + "marker":"square:8:0070C0","legend":"none","categories":CATS,"data":D}) +ch(BL,{"chartType":"radar","radarstyle":"marker","title":"diamond:12", + "marker":"diamond:12","legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"radar","radarstyle":"marker","title":"triangle:10:70AD47", + "marker":"triangle:10:70AD47","legend":"none","categories":CATS,"data":D}) + +new_slide("Backgrounds — chartareafill, plotFill, chartborder, roundedcorners") +ch(TL,{"chartType":"radar","radarstyle":"filled","title":"chartareafill + plotFill + borders", + "chartareafill":"FFF8E7","plotFill":"FAFAFA","chartborder":"000000:1", + "plotborder":"CCCCCC:0.5","legend":"bottom","categories":CATS,"data":D2}) +ch(TR,{"chartType":"radar","radarstyle":"filled","title":"roundedcorners=true", + "roundedcorners":"true","chartborder":"4472C4:2", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BL,{"chartType":"radar","radarstyle":"standard","title":"plotFill=none", + "plotFill":"none","legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"radar","radarstyle":"filled","title":"chartareafill=none", + "chartareafill":"none","legend":"bottom","categories":CATS,"data":D2}) + +new_slide("Presets & per-series Set") +for box,p in zip([TL,TR,BL],["minimal","dark","corporate"]): + ch(box,{"chartType":"radar","radarstyle":"filled","preset":p,"title":f"preset={p}", + "legend":"bottom","categories":CATS,"data":D2}) +ch(BR,{"chartType":"radar","radarstyle":"marker","title":"chart-series Set", + "legend":"bottom","categories":CATS,"data":D2}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[1]", + *P({"name":"Renamed A","color":"C00000","marker":"circle","markerSize":"9"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-scatter.md b/examples/ppt/charts/charts-scatter.md new file mode 100644 index 000000000..ea2d9a6c7 --- /dev/null +++ b/examples/ppt/charts/charts-scatter.md @@ -0,0 +1,64 @@ +# Scatter Charts Showcase — scatterstyle line/lineMarker/marker/smooth/smoothMarker + +Three files work together: + +- **charts-scatter.py** — build script (`python3 charts-scatter.py`). +- **charts-scatter.pptx** — generated deck. +- **charts-scatter.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-scatter.py +# → charts-scatter.pptx +``` + +## Slide map + +``` + Slide 1 scatterstyle variants line / lineMarker / marker / smooth / smoothMarker (5 charts) + Slide 2 Markers marker symbol/size/color + Slide 3 Title & legend + Slide 4 Data labels + Slide 5 Axes min/max, gridlines, log on both axes + Slide 6 Series styling colors, gradient, transparency, outline, shadow + Slide 7 Overlays trendline (linear/poly/exp/log/power/movingAvg), errbars, referenceline + Slide 8 Per-series Set lineWidth/lineDash/marker/markerSize/color/smooth + presets +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=scatter \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-scatter.pptx b/examples/ppt/charts/charts-scatter.pptx new file mode 100644 index 000000000..72dfa97af Binary files /dev/null and b/examples/ppt/charts/charts-scatter.pptx differ diff --git a/examples/ppt/charts/charts-scatter.py b/examples/ppt/charts/charts-scatter.py new file mode 100644 index 000000000..6bb19bdb1 --- /dev/null +++ b/examples/ppt/charts/charts-scatter.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Scatter Charts Showcase — scatterstyle line/lineMarker/marker/smooth/smoothMarker. + +Generates: charts-scatter.pptx + + Slide 1 scatterstyle variants line / lineMarker / marker / smooth / smoothMarker (5 charts) + Slide 2 Markers marker symbol/size/color + Slide 3 Title & legend + Slide 4 Data labels + Slide 5 Axes min/max, gridlines, log on both axes + Slide 6 Series styling colors, gradient, transparency, outline, shadow + Slide 7 Overlays trendline (linear/poly/exp/log/power/movingAvg), errbars, referenceline + Slide 8 Per-series Set lineWidth/lineDash/marker/markerSize/color/smooth + presets + +Usage: + python3 charts-scatter.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-scatter.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +D="A:10,20,18,30,28,40,42,55,52,65" +D2="A:10,20,18,30,28,40,42,55;B:5,12,15,22,25,30,35,40" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("scatterstyle — line / lineMarker / marker / smooth / smoothMarker") +ch(TL,{"chartType":"scatter","scatterstyle":"line","title":"scatterstyle=line", + "legend":"none","data":D}) +ch(TR,{"chartType":"scatter","scatterstyle":"lineMarker","title":"scatterstyle=lineMarker", + "legend":"none","data":D}) +ch(BL,{"chartType":"scatter","scatterstyle":"marker","title":"scatterstyle=marker", + "legend":"none","data":D}) +ch(BR,{"chartType":"scatter","scatterstyle":"smoothMarker","title":"scatterstyle=smoothMarker", + "legend":"none","data":D}) + +new_slide("Markers — symbol / size / color") +ch(TL,{"chartType":"scatter","scatterstyle":"marker","title":"circle:10:FF0000", + "marker":"circle:10:FF0000","legend":"none","data":D}) +ch(TR,{"chartType":"scatter","scatterstyle":"marker","title":"diamond:12:0070C0", + "marker":"diamond:12:0070C0","legend":"none","data":D}) +ch(BL,{"chartType":"scatter","scatterstyle":"marker","title":"square:8:70AD47", + "marker":"square:8:70AD47","legend":"none","data":D}) +ch(BR,{"chartType":"scatter","scatterstyle":"marker","title":"triangle:10", + "marker":"triangle:10","legend":"none","data":D}) + +new_slide("Title & legend") +ch(TL,{"chartType":"scatter","scatterstyle":"smoothMarker","title":"Styled title", + "title.font":"Georgia","title.size":"20","title.color":"4472C4","title.bold":"true", + "legend":"bottom","data":D2}) +ch(TR,{"chartType":"scatter","scatterstyle":"lineMarker","title":"legend=top + legendFont", + "legend":"top","legendFont":"10:333333:Calibri","data":D2}) +ch(BL,{"chartType":"scatter","scatterstyle":"lineMarker","title":"legend.overlay=true", + "legend":"topRight","legend.overlay":"true","data":D2}) +ch(BR,{"chartType":"scatter","scatterstyle":"marker","autotitledeleted":"true","legend":"none","data":D2}) + +new_slide("Data labels — flags + labelfont") +ch(TL,{"chartType":"scatter","scatterstyle":"marker","title":"value","dataLabels":"value", + "labelfont":"9:333333:Calibri","legend":"none","data":D}) +ch(TR,{"chartType":"scatter","scatterstyle":"marker","title":"value,series", + "dataLabels":"value,series","legend":"none","data":D2}) +ch(BL,{"chartType":"scatter","scatterstyle":"marker","title":"labelPos=top", + "dataLabels":"value","labelPos":"top","legend":"none","data":D}) +ch(BR,{"chartType":"scatter","scatterstyle":"marker","title":"dataLabels=none", + "dataLabels":"none","legend":"none","data":D}) + +new_slide("Axes — min/max, gridlines, ticks, log on both axes") +ch(TL,{"chartType":"scatter","scatterstyle":"lineMarker","title":"min/max + titles", + "axismin":"0","axismax":"80","majorunit":"20","axistitle":"Y","cattitle":"X", + "axisfont":"10:333333:Calibri","axisline":"666666:1","axisnumfmt":"#,##0", + "legend":"none","data":D}) +ch(TR,{"chartType":"scatter","scatterstyle":"marker","title":"gridlines + minorGridlines", + "gridlines":"E0E0E0:0.3","minorGridlines":"F0F0F0:0.25","legend":"none","data":D}) +ch(BL,{"chartType":"scatter","scatterstyle":"marker","title":"labelrotation=-30", + "labelrotation":"-30","legend":"none","data":D}) +ch(BR,{"chartType":"scatter","scatterstyle":"marker","title":"logbase=10 (Y)", + "logbase":"10","axismin":"1","axismax":"100","legend":"none", + "data":"A:2,5,8,12,20,40,80"}) + +new_slide("Series styling — colors, gradient, transparency, outline, shadow") +ch(TL,{"chartType":"scatter","scatterstyle":"marker","title":"colors + seriesoutline", + "colors":"4472C4,ED7D31","seriesoutline":"000000:0.5","legend":"bottom","data":D2}) +ch(TR,{"chartType":"scatter","scatterstyle":"marker","title":"gradient + seriesshadow", + "gradient":"FF6600-FFCC00","seriesshadow":"000000-5-45-3-50","legend":"none","data":D}) +ch(BL,{"chartType":"scatter","scatterstyle":"marker","title":"transparency=30", + "transparency":"30","legend":"bottom","data":D2}) +ch(BR,{"chartType":"scatter","scatterstyle":"marker","title":"per-series gradients", + "gradients":"FF0000-0000FF;00FF00-FFFF00","legend":"bottom","data":D2}) + +new_slide("Overlays — trendline (linear/poly/exp/movingAvg), errbars, referenceline") +ch(TL,{"chartType":"scatter","scatterstyle":"marker","title":"trendline=linear", + "trendline":"linear","legend":"none","data":D}) +ch(TR,{"chartType":"scatter","scatterstyle":"marker","title":"trendline=poly:3", + "trendline":"poly:3","legend":"none","data":D}) +ch(BL,{"chartType":"scatter","scatterstyle":"marker","title":"trendline=movingAvg:3", + "trendline":"movingAvg:3","legend":"none","data":D}) +ch(BR,{"chartType":"scatter","scatterstyle":"marker","title":"errbars=stdDev:1", + "errbars":"stdDev:1","legend":"none","data":D}) + +new_slide("Per-series Set + presets — lineWidth/lineDash/marker/markerSize/color/smooth") +for box,p in zip([TL,TR,BL],["minimal","dark","corporate"]): + ch(box,{"chartType":"scatter","scatterstyle":"smoothMarker","preset":p, + "title":f"preset={p}","legend":"bottom","data":D2}) +ch(BR,{"chartType":"scatter","scatterstyle":"lineMarker","title":"chart-series Set per series", + "legend":"bottom","data":D2}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[1]", + *P({"name":"Alpha","color":"C00000","lineWidth":"2.5","lineDash":"solid", + "marker":"circle","markerSize":"10","smooth":"true"})) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[2]", + *P({"name":"Beta","color":"2E75B6","lineWidth":"1.5","lineDash":"dash", + "marker":"diamond","markerSize":"8"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-stock.md b/examples/ppt/charts/charts-stock.md new file mode 100644 index 000000000..9eb32dd1c --- /dev/null +++ b/examples/ppt/charts/charts-stock.md @@ -0,0 +1,64 @@ +# Stock Charts Showcase — High-Low-Close and OHLC variants + +Three files work together: + +- **charts-stock.py** — build script (`python3 charts-stock.py`). +- **charts-stock.pptx** — generated deck. +- **charts-stock.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-stock.py +# → charts-stock.pptx +``` + +## Slide map + +``` + Slide 1 Basic stock 3-series HLC + 4-series OHLC + Slide 2 Hi-low / up-down hilowlines, updownbars + Slide 3 Title & legend + Slide 4 Data labels + Slide 5 Axes min/max, gridlines, axisnumfmt (currency) + Slide 6 Series styling colors, transparency, outline, shadow + Slide 7 Backgrounds chartareafill, plotFill, chartborder + Slide 8 Presets & per-ser preset bundles + chart-series Set +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=stock \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-combo.md](charts-combo.md), [charts-waterfall.md](charts-waterfall.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-stock.pptx b/examples/ppt/charts/charts-stock.pptx new file mode 100644 index 000000000..a8ec76f80 Binary files /dev/null and b/examples/ppt/charts/charts-stock.pptx differ diff --git a/examples/ppt/charts/charts-stock.py b/examples/ppt/charts/charts-stock.py new file mode 100644 index 000000000..09f2bfe1c --- /dev/null +++ b/examples/ppt/charts/charts-stock.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +Stock Charts Showcase — High-Low-Close and OHLC variants. + +Generates: charts-stock.pptx + + Slide 1 Basic stock 3-series HLC + 4-series OHLC + Slide 2 Hi-low / up-down hilowlines, updownbars + Slide 3 Title & legend + Slide 4 Data labels + Slide 5 Axes min/max, gridlines, axisnumfmt (currency) + Slide 6 Series styling colors, transparency, outline, shadow + Slide 7 Backgrounds chartareafill, plotFill, chartborder + Slide 8 Presets & per-ser preset bundles + chart-series Set + +Usage: + python3 charts-stock.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-stock.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +CATS="Mon,Tue,Wed,Thu,Fri" +HLC="High:130,135,140,138,145;Low:118,122,128,125,132;Close:125,130,135,132,140" +OHLC="Open:120,128,130,135,138;High:130,135,140,138,145;Low:118,122,128,125,132;Close:125,130,135,132,140" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("Basic stock — High-Low-Close vs Open-High-Low-Close") +ch(TL,{"chartType":"stock","title":"HLC","legend":"bottom","categories":CATS,"data":HLC}) +ch(TR,{"chartType":"stock","title":"OHLC","legend":"bottom","categories":CATS,"data":OHLC}) +ch(BL,{"chartType":"stock","title":"HLC + dataTable=true","dataTable":"true", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(BR,{"chartType":"stock","title":"OHLC + dataTable=true","dataTable":"true", + "legend":"bottom","categories":CATS,"data":OHLC}) + +new_slide("hilowlines & updownbars") +ch(TL,{"chartType":"stock","title":"hilowlines=true","hilowlines":"true", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(TR,{"chartType":"stock","title":"hilowlines=808080:0.5","hilowlines":"808080:0.5", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(BL,{"chartType":"stock","title":"updownbars=true (OHLC)","updownbars":"true", + "legend":"bottom","categories":CATS,"data":OHLC}) +ch(BR,{"chartType":"stock","title":"updownbars=150:00AA00:FF0000", + "updownbars":"150:00AA00:FF0000","legend":"bottom","categories":CATS,"data":OHLC}) + +new_slide("Title & legend") +ch(TL,{"chartType":"stock","title":"Styled title","title.font":"Georgia","title.size":"20", + "title.color":"4472C4","title.bold":"true","legend":"bottom","categories":CATS,"data":HLC}) +ch(TR,{"chartType":"stock","title":"legend=top + legendFont","legend":"top", + "legendFont":"10:333333:Calibri","categories":CATS,"data":HLC}) +ch(BL,{"chartType":"stock","title":"legend.overlay=true","legend":"topRight", + "legend.overlay":"true","categories":CATS,"data":HLC}) +ch(BR,{"chartType":"stock","autotitledeleted":"true","legend":"none","categories":CATS,"data":HLC}) + +new_slide("Data labels — flags + labelfont") +ch(TL,{"chartType":"stock","title":"dataLabels=value","dataLabels":"value", + "labelfont":"9:333333:Calibri","legend":"bottom","categories":CATS,"data":HLC}) +ch(TR,{"chartType":"stock","title":"value,series","dataLabels":"value,series", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(BL,{"chartType":"stock","title":"value,category","dataLabels":"value,category", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(BR,{"chartType":"stock","title":"dataLabels=none","dataLabels":"none", + "legend":"bottom","categories":CATS,"data":HLC}) + +new_slide("Axes — min/max, gridlines, currency format") +ch(TL,{"chartType":"stock","title":"min/max + titles","axismin":"100","axismax":"160", + "majorunit":"10","axistitle":"Price (USD)","cattitle":"Day", + "axisfont":"10:333333:Calibri","axisnumfmt":"$#,##0.00", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(TR,{"chartType":"stock","title":"gridlines + minorGridlines", + "gridlines":"E0E0E0:0.3","minorGridlines":"F0F0F0:0.25", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(BL,{"chartType":"stock","title":"labelrotation=-30","labelrotation":"-30", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(BR,{"chartType":"stock","title":"dispunits=hundreds","dispunits":"hundreds", + "legend":"bottom","categories":CATS, + "data":"High:13000,13500,14000,13800,14500;Low:11800,12200,12800,12500,13200;Close:12500,13000,13500,13200,14000"}) + +new_slide("Series styling — colors, transparency, outline, shadow") +ch(TL,{"chartType":"stock","title":"colors","colors":"4472C4,ED7D31,70AD47", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(TR,{"chartType":"stock","title":"seriesoutline","seriesoutline":"000000:1", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(BL,{"chartType":"stock","title":"transparency=30","transparency":"30", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(BR,{"chartType":"stock","title":"seriesshadow","seriesshadow":"000000-5-45-3-50", + "legend":"bottom","categories":CATS,"data":HLC}) + +new_slide("Backgrounds — chartareafill, plotFill, chartborder, roundedcorners") +ch(TL,{"chartType":"stock","title":"chartareafill + plotFill + borders", + "chartareafill":"FFF8E7","plotFill":"FAFAFA","chartborder":"000000:1", + "plotborder":"CCCCCC:0.5","legend":"bottom","categories":CATS,"data":HLC}) +ch(TR,{"chartType":"stock","title":"roundedcorners=true","roundedcorners":"true", + "chartborder":"4472C4:2","legend":"bottom","categories":CATS,"data":HLC}) +ch(BL,{"chartType":"stock","title":"plotFill=none","plotFill":"none","gridlines":"none", + "legend":"bottom","categories":CATS,"data":HLC}) +ch(BR,{"chartType":"stock","title":"chartareafill=none","chartareafill":"none", + "legend":"bottom","categories":CATS,"data":HLC}) + +new_slide("Presets & per-series Set") +for box,p in zip([TL,TR,BL],["minimal","dark","corporate"]): + ch(box,{"chartType":"stock","preset":p,"title":f"preset={p}","legend":"bottom", + "categories":CATS,"data":HLC}) +ch(BR,{"chartType":"stock","title":"chart-series Set name+color","legend":"bottom", + "categories":CATS,"data":HLC}) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[1]",*P({"name":"H","color":"00AA00"})) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[2]",*P({"name":"L","color":"C00000"})) +cli("set",FILE,f"/slide[{slide}]/chart[4]/series[3]",*P({"name":"C","color":"4472C4"})) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/charts/charts-waterfall.md b/examples/ppt/charts/charts-waterfall.md new file mode 100644 index 000000000..702a253bb --- /dev/null +++ b/examples/ppt/charts/charts-waterfall.md @@ -0,0 +1,64 @@ +# Waterfall Charts Showcase — increaseColor / decreaseColor / totalColor + +Three files work together: + +- **charts-waterfall.py** — build script (`python3 charts-waterfall.py`). +- **charts-waterfall.pptx** — generated deck. +- **charts-waterfall.md** — this file. + +## Regenerate + +```bash +cd examples/ppt +python3 charts-waterfall.py +# → charts-waterfall.pptx +``` + +## Slide map + +``` + Slide 1 Basic default colors, single dataset + Slide 2 Color schemes increaseColor / decreaseColor / totalColor combinations + Slide 3 Title & legend + Slide 4 Data labels + Slide 5 Axes min/max, gridlines, axisnumfmt (currency) + Slide 6 Backgrounds chartareafill, plotFill, chartborder, roundedcorners + Slide 7 Larger story a real cashflow waterfall with labels + Slide 8 Presets +``` + +## Reference + +```bash +officecli help pptx chart # chart-level properties +officecli help pptx chart-series # per-series Set/Get +officecli help pptx chart-axis # per-axis Set/Get (after creation) +``` + +## Pattern + +```bash +# Create a chart on a slide +officecli add deck.pptx /slide[1] --type chart \ + --prop chartType=waterfall \ + --prop x=1in --prop y=1in --prop width=10in --prop height=5in \ + --prop title="Example" --prop legend=bottom \ + --prop categories="Q1,Q2,Q3,Q4" \ + --prop data="A:60,90,140,180;B:50,75,110,150" + +# Mutate a series after creation +officecli set deck.pptx /slide[1]/chart[1]/series[1] \ + --prop name="Renamed" --prop color=C00000 + +# Mutate an axis after creation +officecli set deck.pptx /slide[1]/chart[1]/axis[@role=value] \ + --prop title="USD" --prop format="\$#,##0" \ + --prop majorGridlines=true --prop min=0 --prop max=200 +``` + +## Related + +- [charts-column.md](charts-column.md), [charts-bar.md](charts-bar.md), [charts-line.md](charts-line.md), [charts-pie.md](charts-pie.md) +- [charts-doughnut.md](charts-doughnut.md), [charts-area.md](charts-area.md), [charts-scatter.md](charts-scatter.md), [charts-bubble.md](charts-bubble.md) +- [charts-radar.md](charts-radar.md), [charts-stock.md](charts-stock.md), [charts-combo.md](charts-combo.md), [charts-3d.md](charts-3d.md) +- [excel charts examples](../excel/) diff --git a/examples/ppt/charts/charts-waterfall.pptx b/examples/ppt/charts/charts-waterfall.pptx new file mode 100644 index 000000000..3f36cec73 Binary files /dev/null and b/examples/ppt/charts/charts-waterfall.pptx differ diff --git a/examples/ppt/charts/charts-waterfall.py b/examples/ppt/charts/charts-waterfall.py new file mode 100644 index 000000000..ac3a8dcd2 --- /dev/null +++ b/examples/ppt/charts/charts-waterfall.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Waterfall Charts Showcase — increaseColor / decreaseColor / totalColor. + +Generates: charts-waterfall.pptx + + Slide 1 Basic default colors, single dataset + Slide 2 Color schemes increaseColor / decreaseColor / totalColor combinations + Slide 3 Title & legend + Slide 4 Data labels + Slide 5 Axes min/max, gridlines, axisnumfmt (currency) + Slide 6 Backgrounds chartareafill, plotFill, chartborder, roundedcorners + Slide 7 Larger story a real cashflow waterfall with labels + Slide 8 Presets + +Usage: + python3 charts-waterfall.py +""" +import subprocess, os, sys, atexit +FILE = os.path.join(os.path.dirname(__file__), "charts-waterfall.pptx") +def cli(*a): + r = subprocess.run(["officecli", *a], capture_output=True, text=True) + if r.returncode: + m=(r.stderr or r.stdout or "").strip().splitlines() + head=m[0][:160] if m else "" + if "UNSUPPORTED" in (r.stderr or ""): + # Forward-compat: skip unsupported props but surface so silent gaps are visible. + print(f" ⚠ {' '.join(a[:3])} → {head}", file=sys.stderr); return + if m: print(f" ! {' '.join(a[:3])} → {head}", file=sys.stderr) + sys.exit(r.returncode) +def P(d): return [x for k,v in d.items() for x in ("--prop", f"{k}={v}")] +slide=0 +def new_slide(t): + global slide; slide+=1 + cli("add",FILE,"/","--type","slide") + cli("add",FILE,f"/slide[{slide}]","--type","shape", + *P({"text":t,"size":24,"bold":"true","autoFit":"normal","x":"0.5in","y":"0.3in","width":"12.3in","height":"0.6in"})) +def ch(box,p): cli("add",FILE,f"/slide[{slide}]","--type","chart",*P({**box,**p})) +TL={"x":"0.3in","y":"1.05in","width":"6.1in","height":"3in"} +TR={"x":"6.95in","y":"1.05in","width":"6.1in","height":"3in"} +BL={"x":"0.3in","y":"4.25in","width":"6.1in","height":"3in"} +BR={"x":"6.95in","y":"4.25in","width":"6.1in","height":"3in"} +HERO={"x":"1in","y":"1.05in","width":"11.3in","height":"6.2in"} +CATS="Start,Q1,Q2,Q3,Q4,End" +D="Cashflow:100,30,-15,40,-10,145" +CATS_LONG="Open,Revenue,COGS,Opex,R&D,Tax,Net" +D_LONG="P&L:100,80,-30,-25,-15,-10,100" + +if os.path.exists(FILE): os.remove(FILE) +cli("create",FILE); cli("open",FILE) +atexit.register(lambda:(cli("close",FILE),cli("validate",FILE))) + +new_slide("Basic waterfall — default colors") +ch(TL,{"chartType":"waterfall","title":"Default colors","legend":"none", + "categories":CATS,"data":D}) +ch(TR,{"chartType":"waterfall","title":"Default + dataTable","dataTable":"true", + "legend":"none","categories":CATS,"data":D}) +ch(BL,{"chartType":"waterfall","title":"With legend","legend":"bottom", + "categories":CATS,"data":D}) +ch(BR,{"chartType":"waterfall","title":"7-step P&L","legend":"none", + "categories":CATS_LONG,"data":D_LONG}) + +new_slide("Color schemes — increaseColor / decreaseColor / totalColor") +ch(TL,{"chartType":"waterfall","title":"green/red/blue (default-ish)", + "increaseColor":"00AA00","decreaseColor":"FF0000","totalColor":"4472C4", + "legend":"none","categories":CATS,"data":D}) +ch(TR,{"chartType":"waterfall","title":"corporate (teal/orange/navy)", + "increaseColor":"008080","decreaseColor":"D86600","totalColor":"1F3864", + "legend":"none","categories":CATS,"data":D}) +ch(BL,{"chartType":"waterfall","title":"monochrome", + "increaseColor":"606060","decreaseColor":"A0A0A0","totalColor":"303030", + "legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"waterfall","title":"vivid", + "increaseColor":"00C853","decreaseColor":"D50000","totalColor":"2962FF", + "legend":"none","categories":CATS,"data":D}) + +new_slide("Title & legend") +ch(TL,{"chartType":"waterfall","title":"Styled title","title.font":"Georgia","title.size":"20", + "title.color":"4472C4","title.bold":"true","legend":"bottom","categories":CATS,"data":D}) +ch(TR,{"chartType":"waterfall","title":"legend=top + legendFont","legend":"top", + "legendFont":"10:333333:Calibri","categories":CATS,"data":D}) +ch(BL,{"chartType":"waterfall","title":"legend.overlay=true","legend":"topRight", + "legend.overlay":"true","categories":CATS,"data":D}) +ch(BR,{"chartType":"waterfall","autotitledeleted":"true","legend":"none", + "categories":CATS,"data":D}) + +new_slide("Data labels — flags + labelfont") +ch(TL,{"chartType":"waterfall","title":"value","dataLabels":"value", + "labelfont":"10:333333:Calibri","legend":"none","categories":CATS,"data":D}) +ch(TR,{"chartType":"waterfall","title":"value,category","dataLabels":"value,category", + "legend":"none","categories":CATS,"data":D}) +ch(BL,{"chartType":"waterfall","title":"value @ outsideEnd","dataLabels":"value", + "labelPos":"outsideEnd","legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"waterfall","title":"dataLabels=none","dataLabels":"none", + "legend":"none","categories":CATS,"data":D}) + +new_slide("Axes — min/max, titles, gridlines, axisnumfmt") +ch(TL,{"chartType":"waterfall","title":"min/max + titles","axismin":"0","axismax":"200", + "majorunit":"50","axistitle":"USD","cattitle":"Phase", + "axisfont":"10:333333:Calibri","axisnumfmt":"$#,##0", + "legend":"none","categories":CATS,"data":D}) +ch(TR,{"chartType":"waterfall","title":"gridlines + minorGridlines", + "gridlines":"E0E0E0:0.3","minorGridlines":"F0F0F0:0.25", + "legend":"none","categories":CATS,"data":D}) +ch(BL,{"chartType":"waterfall","title":"labelrotation=-30","labelrotation":"-30", + "legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"waterfall","title":"dispunits=thousands","dispunits":"thousands", + "legend":"none","categories":CATS, + "data":"USD:100000,30000,-15000,40000,-10000,145000"}) + +new_slide("Backgrounds — chartareafill, plotFill, chartborder, roundedcorners") +ch(TL,{"chartType":"waterfall","title":"chartareafill + chartborder", + "chartareafill":"FFF8E7","chartborder":"000000:1","plotFill":"FAFAFA", + "plotborder":"CCCCCC:0.5","legend":"none","categories":CATS,"data":D}) +ch(TR,{"chartType":"waterfall","title":"roundedcorners=true","roundedcorners":"true", + "chartborder":"4472C4:2","legend":"none","categories":CATS,"data":D}) +ch(BL,{"chartType":"waterfall","title":"plotFill=none","plotFill":"none", + "gridlines":"none","legend":"none","categories":CATS,"data":D}) +ch(BR,{"chartType":"waterfall","title":"chartareafill=none","chartareafill":"none", + "legend":"none","categories":CATS,"data":D}) + +new_slide("Hero cashflow waterfall — full slide with labels") +ch(HERO,{"chartType":"waterfall","title":"FY24 P&L Walk", + "title.font":"Helvetica","title.size":"22","title.bold":"true","title.color":"1F3864", + "increaseColor":"00C853","decreaseColor":"D50000","totalColor":"2962FF", + "dataLabels":"value,category","labelPos":"outsideEnd", + "labelfont":"11:333333:Helvetica","axistitle":"USD","cattitle":"", + "axisnumfmt":"$#,##0","gridlines":"E0E0E0:0.3", + "legend":"none","categories":CATS_LONG,"data":D_LONG}) + +new_slide("Presets") +for box,p in zip([TL,TR,BL,BR],["minimal","dark","corporate","colorful"]): + ch(box,{"chartType":"waterfall","preset":p,"title":f"preset={p}", + "legend":"none","categories":CATS,"data":D}) + +print(f"Done: {FILE} ({slide} slides)") diff --git a/examples/ppt/outputs/beautiful_presentation.pptx b/examples/ppt/outputs/beautiful_presentation.pptx deleted file mode 100644 index beb751493..000000000 Binary files a/examples/ppt/outputs/beautiful_presentation.pptx and /dev/null differ diff --git a/examples/ppt/outputs/claude-morph-template-v39.pptx b/examples/ppt/outputs/claude-morph-template-v39.pptx deleted file mode 100644 index 53b6a7f81..000000000 Binary files a/examples/ppt/outputs/claude-morph-template-v39.pptx and /dev/null differ diff --git a/examples/ppt/outputs/claude-morph-template-v40.pptx b/examples/ppt/outputs/claude-morph-template-v40.pptx deleted file mode 100644 index 1e6f7f593..000000000 Binary files a/examples/ppt/outputs/claude-morph-template-v40.pptx and /dev/null differ diff --git a/examples/ppt/outputs/creative-marketing.pptx b/examples/ppt/outputs/creative-marketing.pptx deleted file mode 100644 index 696f3f33e..000000000 Binary files a/examples/ppt/outputs/creative-marketing.pptx and /dev/null differ diff --git a/examples/ppt/outputs/data_presentation.pptx b/examples/ppt/outputs/data_presentation.pptx deleted file mode 100644 index 62db325c0..000000000 Binary files a/examples/ppt/outputs/data_presentation.pptx and /dev/null differ diff --git a/examples/ppt/outputs/gen-animations-pptx.pptx b/examples/ppt/outputs/gen-animations-pptx.pptx deleted file mode 100644 index 23cac0775..000000000 Binary files a/examples/ppt/outputs/gen-animations-pptx.pptx and /dev/null differ diff --git a/examples/ppt/pictures/pictures-basic.md b/examples/ppt/pictures/pictures-basic.md new file mode 100644 index 000000000..6c4e75812 --- /dev/null +++ b/examples/ppt/pictures/pictures-basic.md @@ -0,0 +1,148 @@ +# Basic PPT Pictures + +Three files work together: + +- **pictures-basic.py** — Python script that generates 3 sample PNGs then builds the deck. +- **pictures-basic.pptx** — The generated 5-slide deck. +- **pictures-basic.md** — This file. + +This example is Python (not shell) because it needs Pillow to generate +the sample images. Once you have your own images on disk, the +underlying `officecli add ... --type picture` calls are identical. + +## Regenerate + +```bash +cd examples/ppt +pip install Pillow +python3 pictures/pictures-basic.py +# → pictures/pictures-basic.pptx +``` + +## Slides + +### Slide 1 — Three forms of `src=` + +`src=` (alias `path=`) accepts: + +| Form | Example | +|---|---| +| File path | `--prop src=/path/to/image.png` | +| URL | `--prop src=https://example.com/logo.png` | +| Data URI | `--prop src="data:image/png;base64,iVBOR..."` | +| Raw bytes | (programmatic — pass via base64 data-URI) | + +The slide shows file path → data-URI → file-with-name. The `name=` +prop overrides the auto-generated `Picture {id}` label on `cNvPr@name`, +useful if you want to look it up by `@name=` later. + +```bash +officecli add file.pptx /slide[1] --type picture \ + --prop src=/path/to/image.png \ + --prop x=0.5in --prop y=1.3in \ + --prop width=3.5in --prop height=2.6in \ + --prop alt="Image description for screen readers" +``` + +`alt=` (aliases: `altText`, `description`) sets accessibility text; +defaults to the source filename if you omit it. + +### Slide 2 — Crop variants + +Five copies of the same image, each cropped differently: + +| Spec | Meaning | +|---|---| +| (no crop) | original aspect | +| `crop=20` | symmetric, 20% off all four edges | +| `crop=10,30` | 10% off top/bot, 30% off left/right (`V,H`) | +| `crop=5,10,40,20` | per-edge: `L,T,R,B` | +| `cropLeft=25 cropTop=25` | individual edges | + +> 💡 `cropLeft=0.1` and `cropLeft=10` both mean 10% — the implementation +> auto-detects fraction (≤1) vs percent (>1). Values 0-100 are +> interpreted as percent; >0..1 as fraction. + +The container `width=`/`height=` stays the same — the visible +fraction of the source image changes. + +### Slide 3 — Rotation + +Six copies arranged in a grid, one per `rotation=`: + +```bash +--prop rotation=0 # default +--prop rotation=30 +--prop rotation=90 +--prop rotation=180 +--prop rotation=270 +--prop rotation=-45 # negative = counter-clockwise +``` + +Alias: `rotate=`. Values stored as `degrees × 60000` in `a:xfrm/@rot`. + +### Slide 4 — Clickable pictures + +`link=` makes the whole picture a hyperlink: + +| Target | Example | +|---|---| +| URL | `--prop link=https://example.com` | +| Slide jump | `--prop link=slide[3]` | +| Named action | `--prop link=nextslide` | +| Other actions | `firstslide`, `lastslide`, `previousslide` | +| Email | `--prop link=mailto:hi@example.com` | +| Clear | `--prop link=none` | + +`tooltip=` sets hover text — must be passed **together** with `link=` +in the same call (standalone tooltip update without link is not +supported by Set). + +```bash +officecli add file.pptx /slide[4] --type picture \ + --prop src=/path/to/logo.png \ + --prop link=https://example.com \ + --prop tooltip="Open homepage" +``` + +### Slide 5 — Set-only effects: brightness / contrast / glow / shadow + +Four props are schema-declared `add: false`, `set: true` — they live +on the picture but can only be applied **after** the picture is +created. Pattern: capture the picture's `@id` path from the Add +response, then Set the effect: + +```bash +PIC=$(officecli add file.pptx /slide[5] --type picture \ + --prop src=/path/to/photo.png ... | awk '/Added/ {print $NF}') + +officecli set file.pptx "$PIC" --prop brightness=40 +officecli set file.pptx "$PIC" --prop contrast=-30 +officecli set file.pptx "$PIC" --prop glow=FFD700-12-75 +officecli set file.pptx "$PIC" --prop shadow=000000-10-45-6-50 +``` + +| Prop | Range / Grammar | Effect | +|---|---|---| +| `brightness` | -100 .. 100 | luminance offset on the blip | +| `contrast` | -100 .. 100 | luminance modulation on the blip | +| `glow` | `color-radius-opacity` (e.g. `FFD700-12-75`) | outer glow | +| `shadow` | `color-blur-angle-dist-opacity` (e.g. `000000-10-45-6-50`) | outer shadow | + +Brightness and contrast can be Set in the **same** call — they share +a single `` element under the blip: + +```bash +officecli set file.pptx "$PIC" --prop brightness=15 --prop contrast=20 +``` + +This slide also exercises `cropBottom=` / `cropRight=` directly by +name (slide 2 only reached them via the four-value `crop=L,T,R,B` +shorthand). + +**Features:** three `src=` forms (file path / URL / data-URI), +`alt=`/`name=` for accessibility and lookup, every `crop=` form +(symmetric, `V,H`, `L,T,R,B`, per-edge `cropLeft`/`cropTop`/`cropRight`/`cropBottom`), +`rotation=` with positive and negative degrees, `link=` to URLs / +slides / named actions, `tooltip=` hover text, Set-only effects +`brightness`/`contrast`/`glow`/`shadow`. diff --git a/examples/ppt/pictures/pictures-basic.pptx b/examples/ppt/pictures/pictures-basic.pptx new file mode 100644 index 000000000..fd8e8c2fa Binary files /dev/null and b/examples/ppt/pictures/pictures-basic.pptx differ diff --git a/examples/ppt/pictures/pictures-basic.py b/examples/ppt/pictures/pictures-basic.py new file mode 100755 index 000000000..0714625ac --- /dev/null +++ b/examples/ppt/pictures/pictures-basic.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +""" +Basic PowerPoint pictures — embed images, position/resize, crop, rotate, hyperlink. + +This script: + 1. Generates 3 sample PNGs (gradient, geometric, photo-like) in a temp dir + 2. Builds a multi-slide PPTX demoing different picture properties: + - slide 1: src= file vs URL vs data-URI (three ways to supply an image) + - slide 2: crop variants — symmetric, vertical/horizontal, per-edge + - slide 3: rotation + - slide 4: hyperlinks (click-to-open URL / jump to slide / next-slide action) + +Requirements: + pip install Pillow + +Usage: + python3 pictures-basic.py +""" + +import base64 +import io +import os +import shutil +import subprocess +import sys +import tempfile + +try: + from PIL import Image, ImageDraw, ImageFont +except ImportError: + print("ERROR: Pillow not installed. Run: pip install Pillow") + sys.exit(1) + + +HERE = os.path.dirname(os.path.abspath(__file__)) +PPTX = os.path.join(HERE, "pictures-basic.pptx") + + +def run(*args): + cmd = ["officecli", *map(str, args)] + print(" $ " + " ".join(cmd)) + r = subprocess.run(cmd, capture_output=True, text=True) + if r.returncode != 0: + print(f" ERROR: {r.stderr.strip() or r.stdout.strip()}") + sys.exit(1) + if r.stdout.strip(): + # show last line only to keep noise down + print(f" {r.stdout.strip().splitlines()[-1]}") + return r.stdout + + +def make_gradient(path, w=400, h=300, c1=(231, 76, 60), c2=(52, 152, 219)): + img = Image.new("RGB", (w, h)) + pix = img.load() + for y in range(h): + t = y / (h - 1) + r = int(c1[0] * (1 - t) + c2[0] * t) + g = int(c1[1] * (1 - t) + c2[1] * t) + b = int(c1[2] * (1 - t) + c2[2] * t) + for x in range(w): + pix[x, y] = (r, g, b) + d = ImageDraw.Draw(img) + d.text((20, 20), "gradient.png", fill=(255, 255, 255)) + img.save(path) + + +def make_geometric(path, w=400, h=300): + img = Image.new("RGB", (w, h), (245, 245, 220)) + d = ImageDraw.Draw(img) + d.ellipse((50, 50, 180, 180), fill=(231, 76, 60), outline=(0, 0, 0), width=3) + d.rectangle((200, 80, 350, 220), fill=(52, 152, 219), outline=(0, 0, 0), width=3) + d.polygon([(120, 200), (60, 270), (180, 270)], + fill=(241, 196, 15), outline=(0, 0, 0)) + d.text((10, 10), "geometric.png", fill=(0, 0, 0)) + img.save(path) + + +def make_photo(path, w=400, h=300): + """A pseudo-photo (radial gradient + noise hint).""" + img = Image.new("RGB", (w, h)) + cx, cy = w / 2, h / 2 + maxd = (cx ** 2 + cy ** 2) ** 0.5 + pix = img.load() + for y in range(h): + for x in range(w): + d = ((x - cx) ** 2 + (y - cy) ** 2) ** 0.5 / maxd + r = int(255 * (1 - d * 0.7)) + g = int(180 * (1 - d * 0.5)) + b = int(80 * (1 - d * 0.3)) + pix[x, y] = (r, g, b) + draw = ImageDraw.Draw(img) + draw.text((10, 10), "photo.png", fill=(255, 255, 255)) + img.save(path) + + +def png_to_data_uri(path): + with open(path, "rb") as f: + data = base64.b64encode(f.read()).decode() + return f"data:image/png;base64,{data}" + + +def main(): + if os.path.exists(PPTX): + os.remove(PPTX) + + workdir = tempfile.mkdtemp(prefix="ocli-pics-") + try: + grad = os.path.join(workdir, "gradient.png") + geo = os.path.join(workdir, "geometric.png") + photo = os.path.join(workdir, "photo.png") + make_gradient(grad) + make_geometric(geo) + make_photo(photo) + + run("create", PPTX) + run("open", PPTX) + + # ── Slide 1: three src= forms ───────────────────────────────────────── + run("add", PPTX, "/", "--type", "slide") + run("add", PPTX, "/slide[1]", "--type", "textbox", + "--prop", "text=Three ways to supply src= (file path / data-URI)", + "--prop", "size=24", "--prop", "bold=true", + "--prop", "x=0.5in", "--prop", "y=0.3in", + "--prop", "width=12in", "--prop", "height=0.6in") + + # 1a. File path + run("add", PPTX, "/slide[1]", "--type", "picture", + "--prop", f"src={grad}", + "--prop", "x=0.5in", "--prop", "y=1.3in", + "--prop", "width=3.5in", "--prop", "height=2.6in", + "--prop", "alt=gradient image from disk") + run("add", PPTX, "/slide[1]", "--type", "textbox", + "--prop", 'text=src=', + "--prop", "size=12", "--prop", "italic=true", + "--prop", "x=0.5in", "--prop", "y=4in", + "--prop", "width=3.5in", "--prop", "height=0.4in") + + # 1b. data-URI + uri = png_to_data_uri(geo) + run("add", PPTX, "/slide[1]", "--type", "picture", + "--prop", f"src={uri}", + "--prop", "x=4.5in", "--prop", "y=1.3in", + "--prop", "width=3.5in", "--prop", "height=2.6in", + "--prop", "alt=geometric shapes embedded as data-URI") + run("add", PPTX, "/slide[1]", "--type", "textbox", + "--prop", 'text=src=data:image/png;base64,...', + "--prop", "size=12", "--prop", "italic=true", + "--prop", "x=4.5in", "--prop", "y=4in", + "--prop", "width=3.5in", "--prop", "height=0.4in") + + # 1c. Another file (use the photo) + run("add", PPTX, "/slide[1]", "--type", "picture", + "--prop", f"src={photo}", + "--prop", "x=8.5in", "--prop", "y=1.3in", + "--prop", "width=3.5in", "--prop", "height=2.6in", + "--prop", "alt=pseudo-photo gradient", + "--prop", "name=hero-photo") + run("add", PPTX, "/slide[1]", "--type", "textbox", + "--prop", 'text=src= + name="hero-photo"', + "--prop", "size=12", "--prop", "italic=true", + "--prop", "x=8.5in", "--prop", "y=4in", + "--prop", "width=3.5in", "--prop", "height=0.4in") + + # ── Slide 2: crop variants ──────────────────────────────────────────── + run("add", PPTX, "/", "--type", "slide") + run("add", PPTX, "/slide[2]", "--type", "textbox", + "--prop", "text=Crop — symmetric / vertical,horizontal / per-edge", + "--prop", "size=24", "--prop", "bold=true", + "--prop", "x=0.5in", "--prop", "y=0.3in", + "--prop", "width=12in", "--prop", "height=0.6in") + + # Original (uncropped reference) + run("add", PPTX, "/slide[2]", "--type", "picture", + "--prop", f"src={geo}", + "--prop", "x=0.5in", "--prop", "y=1.3in", + "--prop", "width=3in", "--prop", "height=2.2in") + run("add", PPTX, "/slide[2]", "--type", "textbox", + "--prop", "text=original (no crop)", "--prop", "size=12", + "--prop", "x=0.5in", "--prop", "y=3.6in", + "--prop", "width=3in", "--prop", "height=0.4in") + + # crop=20 — symmetric all edges + run("add", PPTX, "/slide[2]", "--type", "picture", + "--prop", f"src={geo}", "--prop", "crop=20", + "--prop", "x=4in", "--prop", "y=1.3in", + "--prop", "width=3in", "--prop", "height=2.2in") + run("add", PPTX, "/slide[2]", "--type", "textbox", + "--prop", "text=crop=20 (20% off each edge)", "--prop", "size=12", + "--prop", "x=4in", "--prop", "y=3.6in", + "--prop", "width=3in", "--prop", "height=0.4in") + + # crop=10,30 — vertical 10%, horizontal 30% + run("add", PPTX, "/slide[2]", "--type", "picture", + "--prop", f"src={geo}", "--prop", "crop=10,30", + "--prop", "x=7.5in", "--prop", "y=1.3in", + "--prop", "width=3in", "--prop", "height=2.2in") + run("add", PPTX, "/slide[2]", "--type", "textbox", + "--prop", "text=crop=10,30 (10% top/bot, 30% left/right)", + "--prop", "size=12", + "--prop", "x=7.5in", "--prop", "y=3.6in", + "--prop", "width=3.5in", "--prop", "height=0.4in") + + # Per-edge: cropLeft + cropTop + run("add", PPTX, "/slide[2]", "--type", "picture", + "--prop", f"src={geo}", + "--prop", "cropLeft=25", "--prop", "cropTop=25", + "--prop", "x=0.5in", "--prop", "y=4.3in", + "--prop", "width=3in", "--prop", "height=2.2in") + run("add", PPTX, "/slide[2]", "--type", "textbox", + "--prop", "text=cropLeft=25 + cropTop=25", + "--prop", "size=12", + "--prop", "x=0.5in", "--prop", "y=6.6in", + "--prop", "width=3in", "--prop", "height=0.4in") + + # 4-value crop: left,top,right,bottom + run("add", PPTX, "/slide[2]", "--type", "picture", + "--prop", f"src={geo}", "--prop", "crop=5,10,40,20", + "--prop", "x=4in", "--prop", "y=4.3in", + "--prop", "width=3in", "--prop", "height=2.2in") + run("add", PPTX, "/slide[2]", "--type", "textbox", + "--prop", "text=crop=5,10,40,20 (L,T,R,B)", + "--prop", "size=12", + "--prop", "x=4in", "--prop", "y=6.6in", + "--prop", "width=3in", "--prop", "height=0.4in") + + # ── Slide 3: rotation ───────────────────────────────────────────────── + run("add", PPTX, "/", "--type", "slide") + run("add", PPTX, "/slide[3]", "--type", "textbox", + "--prop", "text=Rotation — degrees clockwise", + "--prop", "size=24", "--prop", "bold=true", + "--prop", "x=0.5in", "--prop", "y=0.3in", + "--prop", "width=12in", "--prop", "height=0.6in") + + positions = [ + (0.5, 1.5, 0), + (4.5, 1.5, 30), + (8.5, 1.5, 90), + (0.5, 4.5, 180), + (4.5, 4.5, 270), + (8.5, 4.5, -45), + ] + for x, y, deg in positions: + run("add", PPTX, "/slide[3]", "--type", "picture", + "--prop", f"src={geo}", + "--prop", f"x={x}in", "--prop", f"y={y}in", + "--prop", "width=3in", "--prop", "height=2.2in", + "--prop", f"rotation={deg}") + run("add", PPTX, "/slide[3]", "--type", "textbox", + "--prop", f"text=rotation={deg}", + "--prop", "size=12", + "--prop", f"x={x}in", "--prop", f"y={y + 2.3}in", + "--prop", "width=3in", "--prop", "height=0.4in") + + # ── Slide 4: clickable hyperlinks on pictures ───────────────────────── + run("add", PPTX, "/", "--type", "slide") + run("add", PPTX, "/slide[4]", "--type", "textbox", + "--prop", "text=Clickable Pictures — link= and tooltip=", + "--prop", "size=24", "--prop", "bold=true", + "--prop", "x=0.5in", "--prop", "y=0.3in", + "--prop", "width=12in", "--prop", "height=0.6in") + + # External URL + run("add", PPTX, "/slide[4]", "--type", "picture", + "--prop", f"src={grad}", + "--prop", "x=0.5in", "--prop", "y=1.5in", + "--prop", "width=3.5in", "--prop", "height=2.6in", + "--prop", "link=https://example.com", + "--prop", "tooltip=Open example.com") + run("add", PPTX, "/slide[4]", "--type", "textbox", + "--prop", "text=link=https://example.com", + "--prop", "size=12", + "--prop", "x=0.5in", "--prop", "y=4.2in", + "--prop", "width=3.5in", "--prop", "height=0.4in") + + # In-deck slide jump + run("add", PPTX, "/slide[4]", "--type", "picture", + "--prop", f"src={geo}", + "--prop", "x=4.5in", "--prop", "y=1.5in", + "--prop", "width=3.5in", "--prop", "height=2.6in", + "--prop", "link=slide[1]", + "--prop", "tooltip=Back to slide 1") + run("add", PPTX, "/slide[4]", "--type", "textbox", + "--prop", "text=link=slide[1] (jump to slide 1)", + "--prop", "size=12", + "--prop", "x=4.5in", "--prop", "y=4.2in", + "--prop", "width=3.5in", "--prop", "height=0.4in") + + # Named action: nextslide + run("add", PPTX, "/slide[4]", "--type", "picture", + "--prop", f"src={photo}", + "--prop", "x=8.5in", "--prop", "y=1.5in", + "--prop", "width=3.5in", "--prop", "height=2.6in", + "--prop", "link=nextslide", + "--prop", "tooltip=Advance one slide") + run("add", PPTX, "/slide[4]", "--type", "textbox", + "--prop", "text=link=nextslide (named action)", + "--prop", "size=12", + "--prop", "x=8.5in", "--prop", "y=4.2in", + "--prop", "width=3.5in", "--prop", "height=0.4in") + + # ── Slide 5: Set-only effects — brightness, contrast, glow, shadow ──── + # These four props are schema-declared add:false / set:true. Pattern: Add + # the picture, then Set the effect on the captured path. Also exercises + # cropBottom / cropRight by their named form (vs the 4-value crop= shape). + run("add", PPTX, "/", "--type", "slide") + run("add", PPTX, "/slide[5]", "--type", "textbox", + "--prop", "text=Picture effects (Set-only) — brightness / contrast / glow / shadow", + "--prop", "size=24", "--prop", "bold=true", + "--prop", "x=0.5in", "--prop", "y=0.3in", + "--prop", "width=13in", "--prop", "height=0.6in") + + def add_pic_and_get_path(slide, x, y, **extra): + """Add a picture and return its DOM path from the success message.""" + args = [ + "add", PPTX, f"/slide[{slide}]", "--type", "picture", + "--prop", f"src={photo}", + "--prop", f"x={x}in", "--prop", f"y={y}in", + "--prop", "width=2.8in", "--prop", "height=2.1in", + ] + for k, v in extra.items(): + args += ["--prop", f"{k}={v}"] + out = run(*args) + # last 'Added picture at ...' line carries the path + for line in reversed(out.splitlines()): + if "Added picture at" in line: + return line.split()[-1] + raise RuntimeError("Could not extract picture path from: " + out) + + def label(slide, x, y, text): + run("add", PPTX, f"/slide[{slide}]", "--type", "textbox", + "--prop", f"text={text}", + "--prop", "size=11", "--prop", "italic=true", + "--prop", f"x={x}in", "--prop", f"y={y}in", + "--prop", "width=2.8in", "--prop", "height=0.4in") + + # Reference (untouched) + ref = add_pic_and_get_path(5, 0.5, 1.2) + label(5, 0.5, 3.4, "(reference)") + + # brightness +40 — lifts mid-tones + p_bright = add_pic_and_get_path(5, 3.6, 1.2) + run("set", PPTX, p_bright, "--prop", "brightness=40") + label(5, 3.6, 3.4, "brightness=40") + + # contrast -30 — flattens + p_con = add_pic_and_get_path(5, 6.7, 1.2) + run("set", PPTX, p_con, "--prop", "contrast=-30") + label(5, 6.7, 3.4, "contrast=-30") + + # brightness + contrast together + p_combo = add_pic_and_get_path(5, 9.8, 1.2) + run("set", PPTX, p_combo, "--prop", "brightness=-20", "--prop", "contrast=40") + label(5, 9.8, 3.4, "brightness=-20 + contrast=40") + + # glow — `color-radius-opacity` + p_glow = add_pic_and_get_path(5, 0.5, 4.2) + run("set", PPTX, p_glow, "--prop", "glow=FFD700-12-75") + label(5, 0.5, 6.4, "glow=FFD700-12-75") + + # shadow — `color-blur-angle-dist-opacity` + p_shadow = add_pic_and_get_path(5, 3.6, 4.2) + run("set", PPTX, p_shadow, "--prop", "shadow=000000-10-45-6-50") + label(5, 3.6, 6.4, "shadow=000000-10-45-6-50") + + # cropRight + cropBottom — by-name form (vs the 4-value crop=) + p_cr = add_pic_and_get_path(5, 6.7, 4.2, cropRight=25, cropBottom=15) + label(5, 6.7, 6.4, "cropRight=25 + cropBottom=15") + + # Everything together: trim corners + brightness + glow + shadow + p_all = add_pic_and_get_path(5, 9.8, 4.2, cropLeft=10, cropTop=10, cropRight=10, cropBottom=10) + run("set", PPTX, p_all, + "--prop", "brightness=15", + "--prop", "contrast=20", + "--prop", "glow=4472C4-8-60", + "--prop", "shadow=000000-6-135-3-40") + label(5, 9.8, 6.4, "trimmed + bright + contrast + glow + shadow") + + run("close", PPTX) + run("validate", PPTX) + print(f"Created: {PPTX}") + + finally: + shutil.rmtree(workdir, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/examples/ppt/presentation.md b/examples/ppt/presentation.md new file mode 100644 index 000000000..205fb1e91 --- /dev/null +++ b/examples/ppt/presentation.md @@ -0,0 +1,6 @@ +# presentation + +TODO: rewrite script with annotated officecli commands. + +See [presentation.sh](presentation.sh) and [presentation.pptx](presentation.pptx). + diff --git a/examples/ppt/presentation.pptx b/examples/ppt/presentation.pptx new file mode 100644 index 000000000..80f06fe71 Binary files /dev/null and b/examples/ppt/presentation.pptx differ diff --git a/examples/ppt/gen-beautiful-pptx.sh b/examples/ppt/presentation.sh similarity index 99% rename from examples/ppt/gen-beautiful-pptx.sh rename to examples/ppt/presentation.sh index d2f46b302..70827b9fa 100755 --- a/examples/ppt/gen-beautiful-pptx.sh +++ b/examples/ppt/presentation.sh @@ -3,7 +3,7 @@ # Deep gradient backgrounds, geometric accents, clean typography set -e -OUT="$(dirname "$0")/beautiful_presentation.pptx" +OUT="$(dirname "$0")/presentation.pptx" rm -f "$OUT" officecli create "$OUT" officecli open "$OUT" diff --git a/examples/ppt/shapes/shapes-basic.md b/examples/ppt/shapes/shapes-basic.md new file mode 100644 index 000000000..c92b62ae2 --- /dev/null +++ b/examples/ppt/shapes/shapes-basic.md @@ -0,0 +1,129 @@ +# Basic PPT Shapes + +Three files work together: + +- **shapes-basic.sh** — Shell script that calls `officecli` to build the deck. +- **shapes-basic.pptx** — The generated 5-slide deck. +- **shapes-basic.md** — This file. + +## Regenerate + +```bash +cd examples/ppt +bash shapes/shapes-basic.sh +# → shapes/shapes-basic.pptx +``` + +## Slides + +### Slide 1 — Geometry preset gallery + +One row of 8 shapes, each using a different `geometry=` preset that the +PPT schema declares: `rect`, `roundRect`, `ellipse`, `triangle`, +`diamond`, `parallelogram`, `rightArrow`, `star5`. + +```bash +officecli add file.pptx /slide[1] --type shape \ + --prop geometry=ellipse \ + --prop x=0.5in --prop y=1.5in --prop width=1.3in --prop height=1.3in \ + --prop fill=4472C4 --prop color=FFFFFF --prop bold=true \ + --prop text="ellipse" +``` + +`geometry=` aliases: `preset` and `shape`. Without it, the default is `rect`. + +### Slide 2 — Fill variations + +Seven shapes, all with the same `geometry=roundRect`, demonstrating every +fill form on one slide: + +| Fill form | Spec | +|---|---| +| Solid hex | `fill=E63946` | +| Theme color | `fill=accent2` (follows deck theme) | +| Linear gradient | `gradient="FF6B6B-4ECDC4-45"` (`C1-C2-ANGLE`) | +| Radial gradient | `gradient="radial:FFE66D-FF6B35-center"` | +| Pattern | `pattern="diagBrick:1D3557:F1FAEE"` (`preset:fg:bg`) | +| Solid + opacity | `fill=2A9D8F --prop opacity=0.4` | +| Outline only | `fill=none --prop line="264653:2.5:solid"` | +| Gradient per-stop positions | `gradient="FF0000@0-FFD700@40-0000FF@100"` | + +> ⚠ `opacity` **requires a fill to attach to**. `opacity=0.4` without +> `fill=` has no effect — see `schemas/help/pptx/shape.json` → +> `opacity.requires: ["fill"]`. + +### Slide 3 — Outline styling + +Two forms for outlines, both supported simultaneously: + +**Compound form** (one string, three parts): + +```bash +--prop line="E63946:3:solid" # color:widthPt:dash +--prop line="1D3557:2:dash" +--prop line="2A9D8F:2.5:dashDot" +``` + +**Per-attribute form**: + +```bash +--prop lineColor=E63946 --prop lineWidth=4pt --prop lineDash=solid +``` + +`cmpd` adds double / triple strokes for one outline: + +```bash +--prop cmpd=dbl # double +--prop cmpd=tri # triple +``` + +`headEnd` / `tailEnd` work on **any** outlined shape, not just connectors — +the two skinny horizontal rectangles at the bottom of slide 3 have arrow +caps on a regular rect. + +### Slide 4 — Rotation + effects + +8 right-arrows showing `rotation=0..270` in 30°/45° steps. Plus three +demo shapes showing effects compound forms supported by the schema: + +```bash +--prop shadow=000000 # outer shadow, default geometry +--prop glow=FFD700 # color glow +--prop reflection=tight # tight | half | full +``` + +`shadow`/`glow` accept a color, or `true` (defaults), or `none` to clear. +Get readback returns rich compound `'#RRGGBBAA-blur-angle-dist-opacity'` +form — see `officecli help pptx get shape | grep -A3 shadow`. + +### Slide 5 — Stroke geometry details + +Three stroke-level props that are easy to miss but matter for precise +diagrams: + +**`lineCap=`** (`flat` | `round` | `square`) — how the stroke +terminates at line endpoints. Shows up most clearly with a thick +dashed line: `round` and `square` both extend past the path's endpoint, +`flat` cuts off exactly at it. + +**`lineJoin=`** (`round` | `bevel` | `miter`) — how corners are +rendered on a stroked shape. Demoed on a thick-outlined triangle so +each value's corner geometry is unambiguous. + +**`lineAlign=`** (`ctr` | `in`) — stroke alignment relative to the +path. `ctr` centers the stroke on the path (default), `in` insets it +fully inside the shape boundary. With a 12pt stroke the difference in +the shape's outer bounds is `~12pt`. + +```bash +--prop lineCap=round # aliases: linecap, line.cap +--prop lineJoin=miter # aliases: linejoin, line.join +--prop lineAlign=in # aliases: linealign, line.align +``` + +**Features:** all 8 schema-declared `geometry` presets, every fill type +(solid/theme/gradient/radial/pattern/opacity/none/per-stop), both line forms +(`line="c:w:d"` compound and per-attribute `lineColor`/`lineWidth`/`lineDash`), +compound strokes (`cmpd=dbl|tri`), arrowheads on shape outlines, +rotation, shadow/glow/reflection effects, full stroke-geometry control +(`lineCap`, `lineJoin`, `lineAlign`). diff --git a/examples/ppt/shapes/shapes-basic.pptx b/examples/ppt/shapes/shapes-basic.pptx new file mode 100644 index 000000000..d09785fd8 Binary files /dev/null and b/examples/ppt/shapes/shapes-basic.pptx differ diff --git a/examples/ppt/shapes/shapes-basic.sh b/examples/ppt/shapes/shapes-basic.sh new file mode 100755 index 000000000..5a3439a56 --- /dev/null +++ b/examples/ppt/shapes/shapes-basic.sh @@ -0,0 +1,245 @@ +#!/bin/bash +# Basic PowerPoint shapes — geometries, fills, outlines, effects, rotation, opacity. +# Demonstrates: --type shape with geometry preset, solid/gradient/pattern/image fills, +# line styling (color/width/dash/arrowheads), rotation, opacity, shadow effects. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/shapes-basic.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 1 — Geometry preset gallery +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="Geometry Presets" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Row of 8 shapes, one per supported preset. +# Schema-declared presets: rect, roundRect, ellipse, triangle, diamond, +# parallelogram, rightArrow, star5 +COL=0 +for preset in rect roundRect ellipse triangle diamond parallelogram rightArrow star5; do + X=$(echo "0.5 + $COL * 1.55" | bc -l) + officecli add "$PPTX" '/slide[1]' --type shape \ + --prop geometry="$preset" \ + --prop x="${X}in" --prop y=1.5in --prop width=1.3in --prop height=1.3in \ + --prop fill=4472C4 --prop color=FFFFFF \ + --prop text="$preset" --prop size=11 --prop bold=true + COL=$((COL + 1)) +done + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 2 — Fill variations on the same geometry +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[2]' --type shape \ + --prop text="Fill Variations" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Solid hex +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=0.5in --prop y=1.3in --prop width=2.5in --prop height=1.5in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true \ + --prop text='fill=E63946' + +# Theme color (follows deck theme) +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=3.3in --prop y=1.3in --prop width=2.5in --prop height=1.5in \ + --prop fill=accent2 --prop color=FFFFFF --prop bold=true \ + --prop text='fill=accent2' + +# Linear gradient (color1-color2-angle) +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=6.1in --prop y=1.3in --prop width=2.5in --prop height=1.5in \ + --prop gradient="FF6B6B-4ECDC4-45" --prop color=FFFFFF --prop bold=true \ + --prop text='gradient linear 45°' + +# Radial gradient +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=8.9in --prop y=1.3in --prop width=2.5in --prop height=1.5in \ + --prop gradient="radial:FFE66D-FF6B35-center" --prop color=000000 --prop bold=true \ + --prop text='gradient radial' + +# Pattern (preset:fg:bg) +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=0.5in --prop y=3.1in --prop width=2.5in --prop height=1.5in \ + --prop pattern="diagBrick:1D3557:F1FAEE" --prop color=FFFFFF --prop bold=true \ + --prop text='pattern diagBrick' + +# Opacity (requires a fill to attach to) +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=3.3in --prop y=3.1in --prop width=2.5in --prop height=1.5in \ + --prop fill=2A9D8F --prop opacity=0.4 --prop color=000000 --prop bold=true \ + --prop text='fill + opacity=0.4' + +# No fill (outline only) +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=6.1in --prop y=3.1in --prop width=2.5in --prop height=1.5in \ + --prop fill=none --prop line="264653:2.5:solid" --prop color=264653 --prop bold=true \ + --prop text='fill=none + outline' + +# Per-stop gradient positions +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=8.9in --prop y=3.1in --prop width=2.5in --prop height=1.5in \ + --prop gradient="FF0000@0-FFD700@40-0000FF@100" --prop color=FFFFFF --prop bold=true \ + --prop text='gradient per-stop' + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 3 — Outline styling (line color / width / dash / caps / arrowheads) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text="Outline Styling" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Compound line form: color:width:dash +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=rect \ + --prop x=0.5in --prop y=1.3in --prop width=3in --prop height=1.2in \ + --prop fill=none --prop line="E63946:3:solid" \ + --prop text='line="E63946:3:solid"' --prop size=12 + +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=rect \ + --prop x=4in --prop y=1.3in --prop width=3in --prop height=1.2in \ + --prop fill=none --prop line="1D3557:2:dash" \ + --prop text='line="1D3557:2:dash"' --prop size=12 + +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=rect \ + --prop x=7.5in --prop y=1.3in --prop width=3in --prop height=1.2in \ + --prop fill=none --prop line="2A9D8F:2.5:dashDot" \ + --prop text='line="2A9D8F:2.5:dashDot"' --prop size=12 + +# Per-attribute form: lineColor + lineWidth + lineDash +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=ellipse \ + --prop x=0.5in --prop y=2.9in --prop width=3in --prop height=1.4in \ + --prop fill=FFE66D --prop lineColor=E63946 --prop lineWidth=4pt --prop lineDash=solid \ + --prop text='separate lineColor/lineWidth/lineDash' --prop size=11 + +# Compound stroke (cmpd=dbl → double line) +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=ellipse \ + --prop x=4in --prop y=2.9in --prop width=3in --prop height=1.4in \ + --prop fill=A8DADC --prop lineColor=1D3557 --prop lineWidth=6pt --prop cmpd=dbl \ + --prop text='cmpd=dbl (double stroke)' --prop size=11 + +# Triple stroke +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=ellipse \ + --prop x=7.5in --prop y=2.9in --prop width=3in --prop height=1.4in \ + --prop fill=A8DADC --prop lineColor=1D3557 --prop lineWidth=8pt --prop cmpd=tri \ + --prop text='cmpd=tri (triple stroke)' --prop size=11 + +# Arrowheads on shape outlines (rightArrow already arrow-shaped — demo headEnd/tailEnd here) +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text="headEnd / tailEnd work on any outline (not just connectors):" \ + --prop size=12 \ + --prop x=0.5in --prop y=4.7in --prop width=12in --prop height=0.4in + +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=rect \ + --prop x=0.5in --prop y=5.2in --prop width=4in --prop height=0.05in \ + --prop fill=none --prop lineColor=000000 --prop lineWidth=2pt \ + --prop headEnd=triangle --prop tailEnd=arrow + +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=rect \ + --prop x=5in --prop y=5.2in --prop width=4in --prop height=0.05in \ + --prop fill=none --prop lineColor=000000 --prop lineWidth=2pt \ + --prop headEnd=diamond --prop tailEnd=oval + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 4 — Rotation, shadow effect, z-order via add order +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[4]' --type shape \ + --prop text="Rotation + Effects" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Rotation in degrees (0..360) +COL=0 +for r in 0 30 60 90 135 180 225 270; do + X=$(echo "0.5 + $COL * 1.55" | bc -l) + officecli add "$PPTX" '/slide[4]' --type shape --prop geometry=rightArrow \ + --prop x="${X}in" --prop y=1.3in --prop width=1.4in --prop height=0.8in \ + --prop fill=4472C4 --prop color=FFFFFF --prop bold=true \ + --prop rotation="$r" --prop text="${r}°" --prop size=11 + COL=$((COL + 1)) +done + +# Shadow effect: shadow=color:blur:offset:direction (handler's compound effect) +officecli add "$PPTX" '/slide[4]' --type shape --prop geometry=roundRect \ + --prop x=1in --prop y=3in --prop width=3.5in --prop height=1.8in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true --prop size=14 \ + --prop text='shadow=000000' \ + --prop shadow=000000 + +officecli add "$PPTX" '/slide[4]' --type shape --prop geometry=roundRect \ + --prop x=5.5in --prop y=3in --prop width=3.5in --prop height=1.8in \ + --prop fill=2A9D8F --prop color=FFFFFF --prop bold=true --prop size=14 \ + --prop text='glow=FFD700' \ + --prop glow=FFD700 + +officecli add "$PPTX" '/slide[4]' --type shape --prop geometry=roundRect \ + --prop x=10in --prop y=3in --prop width=3in --prop height=1.8in \ + --prop fill=F4A261 --prop color=000000 --prop bold=true --prop size=14 \ + --prop text='reflection=tight' \ + --prop reflection=tight + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 5 — Stroke geometry details (lineCap / lineJoin / lineAlign) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[5]' --type shape \ + --prop text="Stroke Geometry — lineCap / lineJoin / lineAlign" \ + --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# lineCap — how the stroke terminates at line endpoints / dash gaps. +# Most visible with a thick dashed stroke. +X=0.5 +for cap in flat round square; do + officecli add "$PPTX" '/slide[5]' --type shape --prop geometry=rect \ + --prop x="${X}in" --prop y=1.5in --prop width=4in --prop height=0.05in \ + --prop fill=none --prop lineColor=1D3557 --prop lineWidth=10pt \ + --prop lineDash=dash --prop lineCap="$cap" + officecli add "$PPTX" '/slide[5]' --type shape \ + --prop text="lineCap=$cap" --prop size=12 \ + --prop x="${X}in" --prop y=1.8in --prop width=4in --prop height=0.4in \ + --prop fill=none --prop line="000000:0:solid" + X=$(echo "$X + 4.3" | bc -l) +done + +# lineJoin — corner style on a stroked shape. +# Most visible on a triangle outline with thick lines. +X=0.5 +for join in round bevel miter; do + officecli add "$PPTX" '/slide[5]' --type shape --prop geometry=triangle \ + --prop x="${X}in" --prop y=2.8in --prop width=2.5in --prop height=2in \ + --prop fill=A8DADC --prop lineColor=E63946 --prop lineWidth=12pt \ + --prop lineJoin="$join" + officecli add "$PPTX" '/slide[5]' --type shape \ + --prop text="lineJoin=$join" --prop size=12 \ + --prop x="${X}in" --prop y=4.9in --prop width=2.5in --prop height=0.4in \ + --prop fill=none --prop line="000000:0:solid" + X=$(echo "$X + 3" | bc -l) +done + +# lineAlign — stroke alignment relative to the path: ctr (centered) vs in (inset). +# Same shape, same border width, only the alignment of the stroke differs. +X=9 +for algn in ctr in; do + officecli add "$PPTX" '/slide[5]' --type shape --prop geometry=rect \ + --prop x="${X}in" --prop y=2.8in --prop width=2in --prop height=2in \ + --prop fill=F4A261 --prop lineColor=1D3557 --prop lineWidth=12pt \ + --prop lineAlign="$algn" + officecli add "$PPTX" '/slide[5]' --type shape \ + --prop text="lineAlign=$algn" --prop size=12 \ + --prop x="${X}in" --prop y=4.9in --prop width=2.5in --prop height=0.4in \ + --prop fill=none --prop line="000000:0:solid" + X=$(echo "$X + 2.5" | bc -l) +done + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/shapes/shapes-connectors.md b/examples/ppt/shapes/shapes-connectors.md new file mode 100644 index 000000000..5a0ec2008 --- /dev/null +++ b/examples/ppt/shapes/shapes-connectors.md @@ -0,0 +1,92 @@ +# Connectors and Groups + +Three files work together: + +- **shapes-connectors.sh** — Shell script that calls `officecli` to build the deck. +- **shapes-connectors.pptx** — The generated 3-slide deck. +- **shapes-connectors.md** — This file. + +## Regenerate + +```bash +cd examples/ppt +bash shapes/shapes-connectors.sh +# → shapes/shapes-connectors.pptx +``` + +## Slides + +### Slide 1 — Connector geometry presets + +Three side-by-side examples, one per preset. Each adds two anchor +ellipses then ties them together with one `--type connector`: + +```bash +# Capture the added shape's @id path +A=$(officecli add file.pptx /slide[1] --type shape --prop geometry=ellipse \ + --prop x=0.5in --prop y=1.5in --prop width=2in --prop height=1.2in \ + --prop fill=4472C4 | awk '/Added/ {print $NF}') +B=$(officecli add file.pptx /slide[1] --type shape --prop geometry=ellipse \ + --prop x=4.5in --prop y=1.5in --prop width=2in --prop height=1.2in \ + --prop fill=E63946 | awk '/Added/ {print $NF}') + +officecli add file.pptx /slide[1] --type connector \ + --prop shape=straight --prop from="$A" --prop to="$B" \ + --prop color=1D3557 --prop lineWidth=2pt --prop tailEnd=triangle +``` + +`shape=` accepts the short presets `straight`, `elbow`, `curve` (or the +OOXML full names: `straightConnector1`, `bentConnector3`, +`curvedConnector3` — see the schema note). + +> 💡 Capture the path Add prints — every `officecli add` echoes +> `Added shape at /slide[N]/shape[@id=M]`. The `@id` form is **stable +> across re-numbering**: when you later `add group ... shapes=...`, +> existing positional indices may shift, but `@id` paths keep working. +> Positional `/slide[N]/shape[M]` works too, but only for read-only +> follow-up commands that don't restructure the slide. + +### Slide 2 — Mini flowchart with attached connectors + +Three boxes (Start → Valid? → End) plus a Retry box. Four connectors: + +- `Start → Valid?` and `Valid? → End` — straight, black, solid +- `Valid? → Retry` and `Retry → Start` — elbow, red, dashed (the "no" loop) + +This is the canonical "flowchart" workflow: shapes hold the boxes, +connectors with `from=` / `to=` hold the relationships. The connector +auto-routes its endpoints based on the source/target bounding boxes, +which is why `elbow` looks correct without any waypoint math. + +Branch labels (`yes` / `no`) are added as plain `--type textbox` with +no fill or outline — same trick used elsewhere when you need bare text +floating over the canvas. + +### Slide 3 — Grouping shapes + +Three overlapping ellipses turned into one group: + +```bash +G1=$(officecli add file.pptx /slide[3] --type shape --prop geometry=ellipse \ + --prop fill=E63946 ... | awk '/Added/ {print $NF}') +G2=$(officecli add ... fill=F4A261 ... | awk '/Added/ {print $NF}') +G3=$(officecli add ... fill=2A9D8F ... | awk '/Added/ {print $NF}') + +officecli add file.pptx /slide[3] --type group \ + --prop shapes="$G1,$G2,$G3" --prop name="Logo" +``` + +The right side of the slide has three independent rectangles in the +same layout for visual comparison — they're addressable as separate +shapes, while the group on the left moves/scales/rotates as one unit +in the PowerPoint UI. + +> ⚠ After `add group`, the message confirms `N shapes moved into +> group. Remaining shape count: K. Shape indices have been +> re-numbered.` This is exactly the trap that makes positional indices +> brittle and `@id` paths essential — see the note on slide 1. + +**Features:** all 3 connector presets (straight/elbow/curve), attached +endpoints via `from=` / `to=` with `@id` paths, dashed connector lines +for branching/loopback semantics, arrowhead styling (`tailEnd=triangle`, +`tailEnd=arrow`), `--type group` with comma-separated `shapes=` paths. diff --git a/examples/ppt/shapes/shapes-connectors.pptx b/examples/ppt/shapes/shapes-connectors.pptx new file mode 100644 index 000000000..b6d5de0de Binary files /dev/null and b/examples/ppt/shapes/shapes-connectors.pptx differ diff --git a/examples/ppt/shapes/shapes-connectors.sh b/examples/ppt/shapes/shapes-connectors.sh new file mode 100755 index 000000000..2ace029b7 --- /dev/null +++ b/examples/ppt/shapes/shapes-connectors.sh @@ -0,0 +1,171 @@ +#!/bin/bash +# Connectors and groups — flowchart-style shapes with attached arrows, then grouped. +# Demonstrates: --type connector with from=/to= shape references, straight/elbow/curve +# presets, arrowheads, --type group with comma-separated shape indices. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/shapes-connectors.pptx" + +# Helper — Add a shape and echo the returned @id path on stdout. +# (officecli prints "Added shape at /slide[N]/shape[@id=M]" — last whitespace-delimited token is the path.) +add_shape_get_path() { + officecli add "$PPTX" "$@" | awk '/Added/ {print $NF; exit}' +} + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 1 — Connector geometry presets (straight / elbow / curve) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="Connector Presets" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +A1=$(add_shape_get_path '/slide[1]' --type shape --prop geometry=ellipse \ + --prop x=0.5in --prop y=1.5in --prop width=2in --prop height=1.2in \ + --prop fill=4472C4 --prop color=FFFFFF --prop bold=true --prop text="A") +B1=$(add_shape_get_path '/slide[1]' --type shape --prop geometry=ellipse \ + --prop x=4.5in --prop y=1.5in --prop width=2in --prop height=1.2in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true --prop text="B") +officecli add "$PPTX" '/slide[1]' --type connector \ + --prop shape=straight --prop from="$A1" --prop to="$B1" \ + --prop color=1D3557 --prop lineWidth=2pt --prop tailEnd=triangle + +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text='straight (default)' --prop size=12 \ + --prop x=0.5in --prop y=2.8in --prop width=6in --prop height=0.4in + +A2=$(add_shape_get_path '/slide[1]' --type shape --prop geometry=ellipse \ + --prop x=0.5in --prop y=3.6in --prop width=2in --prop height=1.2in \ + --prop fill=4472C4 --prop color=FFFFFF --prop bold=true --prop text="A") +B2=$(add_shape_get_path '/slide[1]' --type shape --prop geometry=ellipse \ + --prop x=4.5in --prop y=5in --prop width=2in --prop height=1.2in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true --prop text="B") +officecli add "$PPTX" '/slide[1]' --type connector \ + --prop shape=elbow --prop from="$A2" --prop to="$B2" \ + --prop color=1D3557 --prop lineWidth=2pt --prop tailEnd=triangle + +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text='elbow (bent, 90° turns)' --prop size=12 \ + --prop x=0.5in --prop y=6.3in --prop width=6in --prop height=0.4in + +A3=$(add_shape_get_path '/slide[1]' --type shape --prop geometry=ellipse \ + --prop x=8in --prop y=1.5in --prop width=2in --prop height=1.2in \ + --prop fill=4472C4 --prop color=FFFFFF --prop bold=true --prop text="A") +B3=$(add_shape_get_path '/slide[1]' --type shape --prop geometry=ellipse \ + --prop x=11.5in --prop y=4.5in --prop width=2in --prop height=1.2in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true --prop text="B") +officecli add "$PPTX" '/slide[1]' --type connector \ + --prop shape=curve --prop from="$A3" --prop to="$B3" \ + --prop color=2A9D8F --prop lineWidth=3pt --prop tailEnd=arrow + +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text='curve (smooth Bezier)' --prop size=12 \ + --prop x=7.5in --prop y=6in --prop width=6in --prop height=0.4in + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 2 — Mini flowchart with attached connectors +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[2]' --type shape \ + --prop text="Flowchart with Attached Connectors" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +P1=$(add_shape_get_path '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=0.8in --prop y=2.5in --prop width=2.2in --prop height=1.2in \ + --prop fill=2A9D8F --prop color=FFFFFF --prop bold=true --prop size=16 \ + --prop text="Start") + +P2=$(add_shape_get_path '/slide[2]' --type shape --prop geometry=diamond \ + --prop x=4.5in --prop y=2.3in --prop width=2.8in --prop height=1.6in \ + --prop fill=F4A261 --prop color=000000 --prop bold=true --prop size=14 \ + --prop text="Valid?") + +P3=$(add_shape_get_path '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=9in --prop y=2.5in --prop width=2.2in --prop height=1.2in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true --prop size=16 \ + --prop text="End") + +P4=$(add_shape_get_path '/slide[2]' --type shape --prop geometry=roundRect \ + --prop x=4.7in --prop y=5in --prop width=2.4in --prop height=1in \ + --prop fill=A8DADC --prop color=000000 --prop bold=true --prop size=14 \ + --prop text="Retry") + +# Connect Start → Valid? → End, plus the loopback Valid? → Retry → back to Start +officecli add "$PPTX" '/slide[2]' --type connector \ + --prop shape=straight --prop from="$P1" --prop to="$P2" \ + --prop color=1D3557 --prop lineWidth=2pt --prop tailEnd=triangle + +officecli add "$PPTX" '/slide[2]' --type connector \ + --prop shape=straight --prop from="$P2" --prop to="$P3" \ + --prop color=1D3557 --prop lineWidth=2pt --prop tailEnd=triangle + +officecli add "$PPTX" '/slide[2]' --type connector \ + --prop shape=elbow --prop from="$P2" --prop to="$P4" \ + --prop color=E63946 --prop lineWidth=2pt --prop lineDash=dash --prop tailEnd=triangle + +officecli add "$PPTX" '/slide[2]' --type connector \ + --prop shape=elbow --prop from="$P4" --prop to="$P1" \ + --prop color=E63946 --prop lineWidth=2pt --prop lineDash=dash --prop tailEnd=triangle + +# Branch labels (textbox-style; no fill, transparent outline) +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=7.4in --prop y=2.7in --prop width=1.3in --prop height=0.5in \ + --prop text="yes" --prop size=12 --prop bold=true --prop color=2A9D8F + +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=6in --prop y=4in --prop width=1.3in --prop height=0.5in \ + --prop text="no" --prop size=12 --prop bold=true --prop color=E63946 + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 3 — Grouping shapes +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text="Grouping Shapes" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Three logo-like shapes that we'll group together. +G1=$(add_shape_get_path '/slide[3]' --type shape --prop geometry=ellipse \ + --prop x=1.5in --prop y=2in --prop width=1.4in --prop height=1.4in \ + --prop fill=E63946) +G2=$(add_shape_get_path '/slide[3]' --type shape --prop geometry=ellipse \ + --prop x=2.4in --prop y=2in --prop width=1.4in --prop height=1.4in \ + --prop fill=F4A261 --prop opacity=0.75) +G3=$(add_shape_get_path '/slide[3]' --type shape --prop geometry=ellipse \ + --prop x=3.3in --prop y=2in --prop width=1.4in --prop height=1.4in \ + --prop fill=2A9D8F --prop opacity=0.75) + +# Group them by passing the captured shape paths (comma-separated) +officecli add "$PPTX" '/slide[3]' --type group \ + --prop shapes="$G1,$G2,$G3" --prop name="Logo" + +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop text='Three ellipses grouped (shapes='"$G1,$G2,$G3"').' \ + --prop size=12 \ + --prop x=0.5in --prop y=4in --prop width=12in --prop height=0.5in + +# Three independent boxes for comparison +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=rect \ + --prop x=8in --prop y=2in --prop width=1.4in --prop height=1.4in \ + --prop fill=4472C4 +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=rect \ + --prop x=9.5in --prop y=2in --prop width=1.4in --prop height=1.4in \ + --prop fill=4472C4 +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=rect \ + --prop x=11in --prop y=2in --prop width=1.4in --prop height=1.4in \ + --prop fill=4472C4 + +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop text='Three independent boxes (no group — each addressed separately).' \ + --prop size=12 \ + --prop x=7in --prop y=4in --prop width=6in --prop height=0.5in + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/shapes/shapes-effects.md b/examples/ppt/shapes/shapes-effects.md new file mode 100644 index 000000000..30f0068c5 --- /dev/null +++ b/examples/ppt/shapes/shapes-effects.md @@ -0,0 +1,134 @@ +# Shape Effects and Meta Props + +Three files work together: + +- **shapes-effects.sh** — Shell script that builds the deck (generates a sample PNG inline via Python heredoc, no external image needed). +- **shapes-effects.pptx** — The generated 5-slide deck. +- **shapes-effects.md** — This file. + +This trio fills in everything that `shapes-basic` and `shapes-connectors` +didn't touch: text overflow behavior, mirror flags, image-as-fill, full +3D scene, soft edges, click hyperlinks on the shape itself, and z-order. + +## Regenerate + +```bash +cd examples/ppt +bash shapes/shapes-effects.sh +# → shapes/shapes-effects.pptx +``` + +## Slides + +### Slide 1 — `autoFit` (text overflow behavior) + +Three identical boxes with the same long text, same size, same width. +Only `autoFit=` differs: + +| Value | What happens | +|---|---| +| `none` | Text overflows the box vertically | +| `normal` | Text shrinks until it fits | +| `shape` | Box grows until text fits | + +```bash +--prop autoFit=normal # shrink-to-fit +--prop autoFit=shape # grow-to-fit +--prop autoFit=none # overflow +``` + +Set-only aliases: `true`/`shrink` → `normal`, `resize` → `shape`, +`false` → `none`. + +### Slide 2 — `flipH` / `flipV` (mirror) + +Four `geometry=rightArrow` shapes: original, `flipH`, `flipV`, both. +Flip flags are independent of `rotation=` — combining them composes +predictably (`flipH=true + rotation=90` rotates the mirrored arrow, +not a non-mirrored one). + +```bash +--prop flipH=true # alias: flipHorizontal +--prop flipV=true # alias: flipVertical +``` + +### Slide 3 — `image=` (picture as shape fill) + +Three preset geometries (`ellipse`, `star5`, `diamond`) all filled with +the same PNG. The geometry **clips** the image; the bitmap doesn't drive +the bounding box. + +```bash +officecli add file.pptx /slide[3] --type shape --prop geometry=star5 \ + --prop x=4.5in --prop y=1.5in --prop width=3.5in --prop height=3.5in \ + --prop image=/path/to/photo.png +``` + +> 📌 This is **different from `--type picture`**: +> - `--type picture` embeds the image with its native aspect inside a +> rectangular DrawingML picture frame. +> - `--type shape --prop image=...` uses a shape preset's geometry as +> the clipping outline (DrawingML blipFill on ``). Outline, +> bevel, shadow, etc. all apply to the shape silhouette, not the +> image bounds. + +Aliases: `imagefill`. Get readback surfaces as `image: "true"` (presence +flag, not the path). + +### Slide 4 — 3-D (bevel / depth / lighting / material) + +Six shapes showing the four 3D props composed: + +| Spec | Effect | +|---|---| +| `bevel=circle` | top bevel, default size (6×6 pt) | +| `bevel=angle-8-4` | top bevel, 8pt wide × 4pt high | +| `bevelBottom=circle-4-4` | bottom-face bevel | +| `depth=14pt` | extrusion height — flat shape becomes a 3D solid | +| `lighting=threePt` | scene light rig preset | +| `material=metal` | surface material preset | + +Available `bevel` presets: `angle`, `artDeco`, `circle`, `convex`, +`coolSlant`, `cross`, `divot`, `hardEdge`, `relaxedInset`, `riblet`, +`slope`, `softRound`. + +Available `lighting` rigs: `threePt`, `balanced`, `soft`, `harsh`, +`flood`, `contrasting`, `morning`, `sunrise`, `sunset`, `chilly`, +`freezing`, `flat`, `twoPt`, `glow`, `brightRoom`. + +Available `material` presets: `clear`, `darkEdge` (alias `dkEdge`), +`flat`, `matte`, `metal`, `plastic`, `powder`, `softEdge`, `softMetal`, +`translucentPowder`, `warmMatte`, `wireframe` (alias `wire`). + +### Slide 5 — `softEdge`, `link` + `tooltip`, `name`, `zorder` + +**`softEdge=`** — feathered/blurred edge in points (`0` = sharp, +larger = heavier feather): + +```bash +--prop softEdge=8pt # bare number also accepted (interpreted as pt) +--prop softEdge=none # clear +``` + +**`link=` + `tooltip=` on a shape** — the entire shape becomes +clickable (just like the picture demo, but on a shape): + +```bash +--prop link=https://example.com --prop tooltip="Open homepage" +``` + +Same target grammar as picture: URLs, `slide[N]`, named actions +(`nextslide`, `firstslide`, …), `mailto:`. + +**`name=`** — overrides the auto-generated `Shape {id}` label on +`cNvPr@name`. Useful for `--prop name="cta-button"` so a later +`query` or `set` can target the shape by `@name=cta-button` instead +of guessing the index. + +**`zorder=`** — three overlapping rectangles with explicit stack +position (1 = back). Aliases: `z-order`, `order`. On Add/Set the +shape is moved within the slide's shape tree. + +**Features covered:** `autoFit`, `flipH`, `flipV`, `image` (blipFill on +shape), `bevel`, `bevelBottom`, `depth`, `lighting`, `material`, +`softEdge`, `link`, `tooltip`, `name`, `zorder`. diff --git a/examples/ppt/shapes/shapes-effects.pptx b/examples/ppt/shapes/shapes-effects.pptx new file mode 100644 index 000000000..18a95e48c Binary files /dev/null and b/examples/ppt/shapes/shapes-effects.pptx differ diff --git a/examples/ppt/shapes/shapes-effects.sh b/examples/ppt/shapes/shapes-effects.sh new file mode 100755 index 000000000..f030d2ab4 --- /dev/null +++ b/examples/ppt/shapes/shapes-effects.sh @@ -0,0 +1,245 @@ +#!/bin/bash +# Shape effects and meta — autoFit, flipH/V, image fill, 3D (bevel/depth/lighting/material), +# softEdge, hyperlinks on shape, name override, zorder. +# Covers the shape props NOT touched by shapes-basic / shapes-connectors / textboxes-basic. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/shapes-effects.pptx" + +# Build a tiny PNG once (16×16 magenta+yellow checker) for the image-fill demo. +SAMPLE_PNG="$(mktemp -t ocli-shape-fill.XXXXXX).png" +python3 - "$SAMPLE_PNG" <<'PY' +import struct, zlib, sys +W = H = 64 +rows = [] +for y in range(H): + row = b"\x00" + for x in range(W): + cell = (x // 16 + y // 16) & 1 + row += (b"\xE6\x39\x46" if cell else b"\xFF\xE6\x6D") + rows.append(row) +raw = b"".join(rows) +def chunk(t, d): + return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xffffffff) +png = b"\x89PNG\r\n\x1a\n" +png += chunk(b"IHDR", struct.pack(">IIBBBBB", W, H, 8, 2, 0, 0, 0)) +png += chunk(b"IDAT", zlib.compress(raw)) +png += chunk(b"IEND", b"") +open(sys.argv[1], "wb").write(png) +PY + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 1 — autoFit (text overflow behavior) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop text="autoFit — text overflow behavior" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +LONGTEXT='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.' + +# 'none' — text just overflows the box +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop x=0.5in --prop y=1.5in --prop width=4in --prop height=1.5in \ + --prop fill=F1FAEE --prop size=18 --prop text="$LONGTEXT" \ + --prop autoFit=none + +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop text='autoFit=none (overflows)' --prop size=12 --prop italic=true \ + --prop x=0.5in --prop y=3.2in --prop width=4in --prop height=0.4in + +# 'normal' — shrinks text to fit +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop x=5in --prop y=1.5in --prop width=4in --prop height=1.5in \ + --prop fill=A8DADC --prop size=18 --prop text="$LONGTEXT" \ + --prop autoFit=normal + +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop text='autoFit=normal (text shrinks)' --prop size=12 --prop italic=true \ + --prop x=5in --prop y=3.2in --prop width=4in --prop height=0.4in + +# 'shape' — box resizes to fit text +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop x=9.5in --prop y=1.5in --prop width=4in --prop height=1.5in \ + --prop fill=F4A261 --prop size=18 --prop text="$LONGTEXT" \ + --prop autoFit=shape + +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop text='autoFit=shape (box grows)' --prop size=12 --prop italic=true \ + --prop x=9.5in --prop y=4.5in --prop width=4in --prop height=0.4in + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 2 — flipH / flipV (mirror) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop text="flipH / flipV — mirror" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Original +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=rightArrow \ + --prop x=0.5in --prop y=2in --prop width=2.8in --prop height=1.5in \ + --prop fill=4472C4 --prop color=FFFFFF --prop bold=true --prop text="original" + +# flipH +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=rightArrow \ + --prop x=4in --prop y=2in --prop width=2.8in --prop height=1.5in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true --prop text="flipH=true" \ + --prop flipH=true + +# flipV +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=rightArrow \ + --prop x=7.5in --prop y=2in --prop width=2.8in --prop height=1.5in \ + --prop fill=2A9D8F --prop color=FFFFFF --prop bold=true --prop text="flipV=true" \ + --prop flipV=true + +# flipH + flipV (= rotation 180° visually, but stored as flip flags not rotation) +officecli add "$PPTX" '/slide[2]' --type shape --prop geometry=rightArrow \ + --prop x=11in --prop y=2in --prop width=2.8in --prop height=1.5in \ + --prop fill=F4A261 --prop color=000000 --prop bold=true --prop text="flipH + flipV" \ + --prop flipH=true --prop flipV=true + +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop text='Aliases: flipHorizontal, flipVertical. Flip flags are stored independently of rotation, so flipH + rotate=90 chains predictably.' \ + --prop size=14 --prop italic=true --prop color=666666 \ + --prop x=0.5in --prop y=4in --prop width=13in --prop height=0.6in + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 3 — image fill on a shape (blipFill, NOT --type picture) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop text="image= — picture as shape fill (blipFill)" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# The image fills the shape interior; the geometry preset clips the image. +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=ellipse \ + --prop x=0.5in --prop y=1.5in --prop width=3.5in --prop height=3.5in \ + --prop image="$SAMPLE_PNG" \ + --prop lineColor=1D3557 --prop lineWidth=3pt + +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=star5 \ + --prop x=4.5in --prop y=1.5in --prop width=3.5in --prop height=3.5in \ + --prop image="$SAMPLE_PNG" + +officecli add "$PPTX" '/slide[3]' --type shape --prop geometry=diamond \ + --prop x=8.5in --prop y=1.5in --prop width=3.5in --prop height=3.5in \ + --prop image="$SAMPLE_PNG" \ + --prop lineColor=1D3557 --prop lineWidth=3pt + +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop text='image="/path/to/photo.png" turns the shape into a clipped picture — different element from --type picture, which embeds the bitmap with its native bounding box.' \ + --prop size=14 --prop italic=true --prop color=666666 \ + --prop x=0.5in --prop y=5.5in --prop width=13in --prop height=1in + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 4 — 3D effects (bevel, bevelBottom, depth, lighting, material) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop text="3D — bevel / depth / lighting / material" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Bevel top, default size +officecli add "$PPTX" '/slide[4]' --type shape --prop geometry=roundRect \ + --prop x=0.5in --prop y=1.4in --prop width=3in --prop height=1.8in \ + --prop fill=4472C4 --prop color=FFFFFF --prop bold=true --prop size=14 \ + --prop text='bevel=circle' \ + --prop bevel=circle + +# Bevel top + bottom with explicit widths +officecli add "$PPTX" '/slide[4]' --type shape --prop geometry=roundRect \ + --prop x=4in --prop y=1.4in --prop width=3in --prop height=1.8in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true --prop size=14 \ + --prop text='bevel=angle-8-4 + bevelBottom=circle-4-4' \ + --prop bevel=angle-8-4 --prop bevelBottom=circle-4-4 + +# Extrusion depth +officecli add "$PPTX" '/slide[4]' --type shape --prop geometry=roundRect \ + --prop x=7.5in --prop y=1.4in --prop width=3in --prop height=1.8in \ + --prop fill=2A9D8F --prop color=FFFFFF --prop bold=true --prop size=14 \ + --prop text='depth=14pt + bevel=softRound' \ + --prop depth=14pt --prop bevel=softRound + +# Lighting + material combos +officecli add "$PPTX" '/slide[4]' --type shape --prop geometry=ellipse \ + --prop x=0.5in --prop y=3.7in --prop width=3in --prop height=1.8in \ + --prop fill=F4A261 --prop color=000000 --prop bold=true --prop size=12 \ + --prop text='bevel=circle-8 depth=10 lighting=threePt material=metal' \ + --prop bevel=circle-8 --prop depth=10 --prop lighting=threePt --prop material=metal + +officecli add "$PPTX" '/slide[4]' --type shape --prop geometry=ellipse \ + --prop x=4in --prop y=3.7in --prop width=3in --prop height=1.8in \ + --prop fill=A8DADC --prop color=000000 --prop bold=true --prop size=12 \ + --prop text='lighting=balanced material=plastic' \ + --prop bevel=circle-6 --prop depth=8 --prop lighting=balanced --prop material=plastic + +officecli add "$PPTX" '/slide[4]' --type shape --prop geometry=ellipse \ + --prop x=7.5in --prop y=3.7in --prop width=3in --prop height=1.8in \ + --prop fill=FFD700 --prop color=000000 --prop bold=true --prop size=12 \ + --prop text='lighting=harsh material=warmMatte' \ + --prop bevel=circle-6 --prop depth=8 --prop lighting=harsh --prop material=warmMatte + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 5 — softEdge + link + tooltip + name + zorder +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[5]' --type textbox \ + --prop text="softEdge / link / name / zorder" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# softEdge — feathered/blurred edge in points +officecli add "$PPTX" '/slide[5]' --type shape --prop geometry=ellipse \ + --prop x=0.5in --prop y=1.5in --prop width=3in --prop height=2in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true \ + --prop text='softEdge=0 (sharp)' --prop softEdge=0 + +officecli add "$PPTX" '/slide[5]' --type shape --prop geometry=ellipse \ + --prop x=4in --prop y=1.5in --prop width=3in --prop height=2in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true \ + --prop text='softEdge=8pt' --prop softEdge=8pt + +officecli add "$PPTX" '/slide[5]' --type shape --prop geometry=ellipse \ + --prop x=7.5in --prop y=1.5in --prop width=3in --prop height=2in \ + --prop fill=E63946 --prop color=FFFFFF --prop bold=true \ + --prop text='softEdge=20pt (heavy feather)' --prop softEdge=20pt + +# link + tooltip on a shape — entire shape becomes clickable +officecli add "$PPTX" '/slide[5]' --type shape --prop geometry=roundRect \ + --prop x=0.5in --prop y=4in --prop width=4in --prop height=1in \ + --prop fill=2A9D8F --prop color=FFFFFF --prop bold=true --prop size=16 \ + --prop text="Click me → example.com" \ + --prop link=https://example.com --prop tooltip="Open example.com" \ + --prop name="cta-button" + +officecli add "$PPTX" '/slide[5]' --type textbox \ + --prop text='link=https://example.com tooltip="Open example.com" name="cta-button"' \ + --prop size=12 --prop italic=true --prop color=666666 \ + --prop x=0.5in --prop y=5.1in --prop width=6in --prop height=0.4in + +# zorder — three overlapping shapes with explicit stack order +officecli add "$PPTX" '/slide[5]' --type shape --prop geometry=rect \ + --prop x=8in --prop y=4in --prop width=2.5in --prop height=2.5in \ + --prop fill=4472C4 --prop name="back" --prop zorder=1 \ + --prop color=FFFFFF --prop bold=true --prop text="back (zorder=1)" + +officecli add "$PPTX" '/slide[5]' --type shape --prop geometry=rect \ + --prop x=9in --prop y=4.5in --prop width=2.5in --prop height=2.5in \ + --prop fill=E63946 --prop name="middle" --prop zorder=2 \ + --prop color=FFFFFF --prop bold=true --prop text="middle (zorder=2)" + +officecli add "$PPTX" '/slide[5]' --type shape --prop geometry=rect \ + --prop x=10in --prop y=5in --prop width=2.5in --prop height=2.5in \ + --prop fill=F4A261 --prop name="front" --prop zorder=3 \ + --prop color=000000 --prop bold=true --prop text="front (zorder=3)" + +officecli close "$PPTX" +officecli validate "$PPTX" +rm -f "$SAMPLE_PNG" +echo "Created: $PPTX" diff --git a/examples/ppt/shapes/shapes-typography.md b/examples/ppt/shapes/shapes-typography.md new file mode 100644 index 000000000..48a2a8194 --- /dev/null +++ b/examples/ppt/shapes/shapes-typography.md @@ -0,0 +1,136 @@ +# Shape Typography + +Three files work together: + +- **shapes-typography.sh** — Shell script that builds the deck. +- **shapes-typography.pptx** — The generated 4-slide deck. +- **shapes-typography.md** — This file. + +Fills in the typography props NOT touched by `textboxes-basic`: +paragraph-level spacing, character spacing, kerning threshold, +case rendering, BCP-47 language tag, RTL direction, and the +complex-script font slot. + +## Regenerate + +```bash +cd examples/ppt +bash shapes/shapes-typography.sh +# → shapes/shapes-typography.pptx +``` + +## Slides + +### Slide 1 — Paragraph spacing + +Three identical 3-paragraph blocks; only the spacing props differ: + +| Spec | Effect | +|---|---| +| default | tight default leading | +| `lineSpacing=1.5x` | 150% line height (multiplier) | +| `spaceBefore=12pt + spaceAfter=12pt` | gap between paragraphs | + +`lineSpacing` accepts multiple input forms via `SpacingConverter`: + +```bash +--prop lineSpacing=1.5x # multiplier +--prop lineSpacing=150% # percent form +--prop lineSpacing=18pt # fixed +--prop lineSpacing=0.5cm # length +``` + +`spaceBefore` / `spaceAfter` accept any length string (`12pt`, +`0.5cm`, `0.25in`). Aliases: `spacebefore`, `spaceafter`, +`linespacing`. All three props can also be set on individual +paragraphs (`--type paragraph --prop spaceBefore=...`) for +mixed-spacing layouts. + +### Slide 2 — Character spacing, kerning, case + +**`spacing=`** — character spacing in 1/100 pt (a.k.a. tracking). +Same string repeated at four spacing values shows the effect: + +```bash +--prop spacing=-50 # negative = tighter +--prop spacing=200 # positive = looser +--prop spacing=500 # very loose, theatrical +``` + +Aliases: `spc`, `charspacing`, `letterspacing`. + +**`kern=`** — minimum font size (in 1/100 pt) at which OpenType +kerning pairs are applied: + +```bash +--prop kern=0 # disabled (no kerning at any size) +--prop kern=1 # enabled at all sizes (1 = 0.01pt threshold) +--prop kern=1200 # only kern from 12pt up +``` + +**`cap=`** — letter-case rendering mode: + +| Value | Aliases | Effect | +|---|---|---| +| `none` | (default) | text as written | +| `small` | `smallCaps`, `smallcaps` | mAJUSCULE for lower-case | +| `all` | `allCaps`, `allcaps` | MAJUSCULES throughout | + +The underlying string is unchanged; only the visual rendering differs, +so search/copy preserves case. + +### Slide 3 — `direction=rtl` + `font.cs` + +The same Arabic string rendered twice: + +- **Left box (LTR, default):** `font.cs="Arabic Typesetting"` only — + the trailing digits/punctuation land in their LTR-natural slots. +- **Right box (RTL):** `direction=rtl` + `align=right` reorders the + whole paragraph right-to-left, which is what Arabic readers expect. + +```bash +officecli add file.pptx /slide[3] --type textbox \ + --prop text="مرحبا بالعالم — 2026" \ + --prop direction=rtl \ + --prop font.cs="Arabic Typesetting" \ + --prop align=right +``` + +`direction=` aliases: `dir`, `rtl`. Same machinery covers Hebrew, +Urdu, Persian, Yiddish, etc. — pick the right `font.cs` face. + +> 💡 `font.cs` is the **complex-script** font slot (DrawingML +> `a:cs`). PowerPoint picks it automatically for code points in +> the Arabic/Hebrew/Thai/Indic ranges. Latin and East-Asian chars +> in the same paragraph still use `font.latin` / `font.ea`. + +### Slide 4 — Bare `font` + BCP-47 `lang` tag + +**Bare `font=`** targets BOTH the Latin (`a:latin`) and EastAsian +(`a:ea`) slots in one shot — useful for documents in a single script. +Per-script `font.latin` / `font.ea` / `font.cs` overrides give finer +control when you need different faces per script. + +```bash +--prop font="Times New Roman" # both Latin and EA +--prop font.latin=Georgia \ +--prop font.ea="Yu Mincho" # per-script +``` + +**`lang=`** — BCP-47 language tag on the first run's `rPr/@lang`. +Affects spellcheck, hyphenation, and font fallback in PowerPoint: + +```bash +--prop lang=en-US # US English (default) +--prop lang=en-GB # British English +--prop lang=fr-FR # French +--prop lang=ja-JP # Japanese +--prop lang=ar-SA # Arabic (Saudi Arabia) +``` + +Aliases: `altLang`, `altlang`. Tag is validated against BCP-47 shape; +max 35 characters. + +**Features covered:** `lineSpacing`, `spaceBefore`, `spaceAfter`, +`spacing` (char tracking), `kern` threshold, `cap=none|small|all`, +`direction=rtl`, `font` (bare), `font.cs`, `lang` BCP-47 tag. diff --git a/examples/ppt/shapes/shapes-typography.pptx b/examples/ppt/shapes/shapes-typography.pptx new file mode 100644 index 000000000..3fc2554c4 Binary files /dev/null and b/examples/ppt/shapes/shapes-typography.pptx differ diff --git a/examples/ppt/shapes/shapes-typography.sh b/examples/ppt/shapes/shapes-typography.sh new file mode 100755 index 000000000..23a6506f6 --- /dev/null +++ b/examples/ppt/shapes/shapes-typography.sh @@ -0,0 +1,216 @@ +#!/bin/bash +# Shape typography — paragraph spacing, character spacing, kerning, case, BCP-47 lang, +# RTL direction, complex-script (Arabic) font slot. +# Covers the typography props NOT touched by textboxes-basic. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/shapes-typography.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +LOREM='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt.' + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 1 — Paragraph spacing (lineSpacing / spaceBefore / spaceAfter) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop text="lineSpacing / spaceBefore / spaceAfter" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Reference (tight spacing) — default +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop x=0.5in --prop y=1.2in --prop width=4in --prop height=3.5in \ + --prop fill=F1FAEE --prop size=14 --prop text="$LOREM" +officecli add "$PPTX" '/slide[1]/shape[2]' --type paragraph --prop text="$LOREM" +officecli add "$PPTX" '/slide[1]/shape[2]' --type paragraph --prop text="$LOREM" + +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop text='default (no spacing props set)' --prop size=12 --prop italic=true \ + --prop x=0.5in --prop y=4.8in --prop width=4in --prop height=0.4in + +# lineSpacing=1.5x +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop x=5in --prop y=1.2in --prop width=4in --prop height=3.5in \ + --prop fill=A8DADC --prop size=14 --prop text="$LOREM" --prop lineSpacing=1.5x +officecli add "$PPTX" '/slide[1]/shape[4]' --type paragraph --prop text="$LOREM" --prop lineSpacing=1.5x +officecli add "$PPTX" '/slide[1]/shape[4]' --type paragraph --prop text="$LOREM" --prop lineSpacing=1.5x + +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop text='lineSpacing=1.5x (multiplier; also accepts 150% / 18pt)' \ + --prop size=12 --prop italic=true \ + --prop x=5in --prop y=4.8in --prop width=4in --prop height=0.4in + +# spaceBefore + spaceAfter on each paragraph +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop x=9.5in --prop y=1.2in --prop width=4in --prop height=3.5in \ + --prop fill=F4A261 --prop size=14 --prop text="$LOREM" \ + --prop spaceBefore=12pt --prop spaceAfter=12pt +officecli add "$PPTX" '/slide[1]/shape[6]' --type paragraph --prop text="$LOREM" \ + --prop spaceBefore=12pt --prop spaceAfter=12pt +officecli add "$PPTX" '/slide[1]/shape[6]' --type paragraph --prop text="$LOREM" \ + --prop spaceBefore=12pt --prop spaceAfter=12pt + +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop text='spaceBefore=12pt spaceAfter=12pt' --prop size=12 --prop italic=true \ + --prop x=9.5in --prop y=4.8in --prop width=4in --prop height=0.4in + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 2 — Character spacing, kerning, all/small caps +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop text="spacing / kern / cap" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Character spacing (1/100 pt; positive = looser, negative = tighter) +SAMPLE='Tight Loose Spacing TYPOGRAPHY' + +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=1.5in --prop width=13in --prop height=0.8in \ + --prop size=22 --prop bold=true --prop text="$SAMPLE (default)" + +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=2.3in --prop width=13in --prop height=0.8in \ + --prop size=22 --prop bold=true --prop text="$SAMPLE (spacing=-50)" \ + --prop spacing=-50 + +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=3.1in --prop width=13in --prop height=0.8in \ + --prop size=22 --prop bold=true --prop text="$SAMPLE (spacing=200)" \ + --prop spacing=200 + +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=3.9in --prop width=13in --prop height=0.8in \ + --prop size=22 --prop bold=true --prop text="$SAMPLE (spacing=500)" \ + --prop spacing=500 + +# Kerning threshold — minimum font size (in 1/100 pt) at which kerning kicks in +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=5in --prop width=6in --prop height=0.8in \ + --prop size=18 --prop text="AVATAR Yawning (kern=1) — kern on from 0.01pt" \ + --prop kern=1 + +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=7in --prop y=5in --prop width=6in --prop height=0.8in \ + --prop size=18 --prop text="AVATAR Yawning (kern=0) — kern OFF" \ + --prop kern=0 + +# Case rendering +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=6in --prop width=4in --prop height=0.8in \ + --prop size=18 --prop text="cap=none — Default case" --prop cap=none + +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=4.7in --prop y=6in --prop width=4in --prop height=0.8in \ + --prop size=18 --prop text="cap=small — Small caps" --prop cap=small + +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=8.9in --prop y=6in --prop width=4in --prop height=0.8in \ + --prop size=18 --prop text="cap=all — All caps" --prop cap=all + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 3 — direction=rtl + font.cs (Arabic / complex-script) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop text="direction=rtl + font.cs (complex script)" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# LTR Arabic — punctuation/digits end up in left-to-right order +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop x=0.5in --prop y=1.5in --prop width=6in --prop height=1.2in \ + --prop fill=F1FAEE --prop size=24 --prop bold=true \ + --prop text="مرحبا بالعالم — 2026" \ + --prop font.cs="Arabic Typesetting" + +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop text='direction=ltr (default) + font.cs="Arabic Typesetting"' \ + --prop size=12 --prop italic=true --prop color=666666 \ + --prop x=0.5in --prop y=2.8in --prop width=6in --prop height=0.4in + +# RTL Arabic — paragraph flows right-to-left +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop x=7in --prop y=1.5in --prop width=6in --prop height=1.2in \ + --prop fill=A8DADC --prop size=24 --prop bold=true \ + --prop text="مرحبا بالعالم — 2026" \ + --prop direction=rtl --prop font.cs="Arabic Typesetting" --prop align=right + +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop text='direction=rtl + align=right (aliases: dir, rtl)' \ + --prop size=12 --prop italic=true --prop color=666666 \ + --prop x=7in --prop y=2.8in --prop width=6in --prop height=0.4in + +# Hebrew +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop x=0.5in --prop y=3.7in --prop width=12.5in --prop height=1.5in \ + --prop fill=F4A261 --prop size=24 --prop bold=true \ + --prop text="שלום עולם — Hebrew demo" \ + --prop direction=rtl --prop font.cs="Arial Hebrew" --prop align=right + +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop text='Same RTL machinery covers Hebrew, Urdu, Persian etc. — pick the appropriate font.cs face.' \ + --prop size=12 --prop italic=true --prop color=666666 \ + --prop x=0.5in --prop y=5.3in --prop width=12.5in --prop height=0.4in + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 4 — Bare 'font' + BCP-47 lang tag +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop text="font (bare) + lang BCP-47" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Bare 'font' targets BOTH Latin and EastAsian slots in one shot +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=0.5in --prop y=1.5in --prop width=6in --prop height=1.5in \ + --prop fill=F1FAEE --prop size=22 \ + --prop text="Bare font sets Latin + EastAsian" \ + --prop font="Times New Roman" + +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop text='font="Times New Roman" (sets a:latin AND a:ea)' \ + --prop size=12 --prop italic=true \ + --prop x=0.5in --prop y=3.1in --prop width=6in --prop height=0.4in + +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=7in --prop y=1.5in --prop width=6in --prop height=1.5in \ + --prop fill=A8DADC --prop size=22 \ + --prop text="Per-script gives finer control" \ + --prop font.latin="Georgia" --prop font.ea="Yu Mincho" + +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop text='font.latin=Georgia + font.ea="Yu Mincho" (a:latin / a:ea)' \ + --prop size=12 --prop italic=true \ + --prop x=7in --prop y=3.1in --prop width=6in --prop height=0.4in + +# BCP-47 language tags — affects spellcheck, hyphenation, font fallback +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=0.5in --prop y=3.8in --prop width=4in --prop height=1in \ + --prop fill=F4A261 --prop size=18 --prop text="Color or colour?" --prop lang=en-GB +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop text='lang=en-GB (British English spellcheck)' --prop size=12 --prop italic=true \ + --prop x=0.5in --prop y=4.9in --prop width=4in --prop height=0.4in + +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=4.7in --prop y=3.8in --prop width=4in --prop height=1in \ + --prop fill=F4A261 --prop size=18 --prop text="Couleur en français" --prop lang=fr-FR +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop text='lang=fr-FR' --prop size=12 --prop italic=true \ + --prop x=4.7in --prop y=4.9in --prop width=4in --prop height=0.4in + +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=8.9in --prop y=3.8in --prop width=4in --prop height=1in \ + --prop fill=F4A261 --prop size=18 --prop text="日本語のテスト" --prop lang=ja-JP \ + --prop font.ea="Yu Mincho" +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop text='lang=ja-JP + font.ea="Yu Mincho"' --prop size=12 --prop italic=true \ + --prop x=8.9in --prop y=4.9in --prop width=4in --prop height=0.4in + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/tables/tables-basic.md b/examples/ppt/tables/tables-basic.md new file mode 100644 index 000000000..a8a226462 --- /dev/null +++ b/examples/ppt/tables/tables-basic.md @@ -0,0 +1,71 @@ +# Basic PPT Tables + +Three files work together: + +- **tables-basic.sh** — Shell script that calls `officecli` to build the deck. +- **tables-basic.pptx** — The generated 3-slide deck. +- **tables-basic.md** — This file. + +## Regenerate + +```bash +cd examples/ppt +bash tables-basic.sh +# → tables-basic.pptx +``` + +## Slides + +### Slide 1 — Inline `data=` seed + +Whole table populated in a single command with `data="H1,H2;R1C1,R1C2"` +(commas separate cells, semicolons separate rows). + +```bash +officecli add file.pptx /slide[1] --type table \ + --prop x=0.5in --prop y=1.2in --prop width=12in --prop height=2in \ + --prop headerFill=4472C4 --prop bodyFill=DEEAF6 \ + --prop data="Region,Q1,Q2,Q3,Q4;North,120,135,142,168;South,98,110,121,140;East,165,178,190,205" +``` + +> ⚠ `headerFill` / `bodyFill` are a **per-cell stamp** applied at table +> creation, not a table-level property. If you later run `add row` or +> `add column`, the new cells will not auto-color — you have to set +> their `fill` explicitly. Want appended rows/columns to follow the +> coloring automatically? Use a theme style instead: +> `--prop style=medium2 --prop firstRow=true --prop bandedRows=true`. +> See [tables-rows-cols.md](tables-rows-cols.md) for the side-by-side +> comparison. + +### Slide 2 — Empty grid + per-cell `set` + +Reserve the grid with `rows`/`cols`, then set each cell. Useful when cell +values aren't known up-front, or different cells need different styling. + +```bash +officecli add file.pptx /slide[2] --type table \ + --prop rows=4 --prop cols=3 --prop headerFill=2E75B6 + +officecli set file.pptx /slide[2]/table[1]/tr[1]/tc[1] \ + --prop text="Product" --prop bold=true --prop color=FFFFFF +``` + +### Slide 3 — Cell fill variations + +`fill` (alias `background`/`shd`) accepts several forms: + +| Form | Example | +|---|---| +| Solid hex | `fill=FF0000` or `fill=#FF0000` | +| Named color | `fill=red` | +| `rgb(...)` | `fill="rgb(255,0,0)"` | +| Theme color | `fill=accent1` (also `accent2..6`, `dk1`, `dk2`, `lt1`, `lt2`, `hyperlink`) | +| Gradient | `fill="FF0000-0000FF-90"` — `"COLOR1-COLOR2[-ANGLE]"`, angle in degrees | +| No fill | `fill=none` — transparent (page bg shows through) | + +Theme colors follow the deck theme — recolor the deck and the table follows. +Hex/named colors are absolute. + +**Features:** `data=` inline seed, `headerFill`/`bodyFill`, `rows`/`cols`, +per-cell `text`/`bold`/`color`/`fill` (solid/named/rgb/theme/gradient/none), +EMU-parseable dimensions (`0.5in`). diff --git a/examples/ppt/tables/tables-basic.pptx b/examples/ppt/tables/tables-basic.pptx new file mode 100644 index 000000000..dff6f3ba7 Binary files /dev/null and b/examples/ppt/tables/tables-basic.pptx differ diff --git a/examples/ppt/tables/tables-basic.sh b/examples/ppt/tables/tables-basic.sh new file mode 100644 index 000000000..0f7e106c0 --- /dev/null +++ b/examples/ppt/tables/tables-basic.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Basic PowerPoint table — header row, body rows, fills, font sizing. +# Demonstrates: add table with inline `data=` CSV, headerFill/bodyFill, +# per-cell text override via set, table dimensions (x/y/width/height). + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/tables-basic.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +# --- Slide 1: minimal 3x3 table seeded inline --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="Basic Table — Inline Data" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# 'data=' uses CSV (comma = cell sep, semicolon = row sep). +officecli add "$PPTX" '/slide[1]' --type table \ + --prop x=0.5in --prop y=1.2in --prop width=12in --prop height=2in \ + --prop headerFill=4472C4 --prop bodyFill=DEEAF6 \ + --prop data="Region,Q1,Q2,Q3,Q4;North,120,135,142,168;South,98,110,121,140;East,165,178,190,205" + +# --- Slide 2: explicit rows/cols then per-cell text --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[2]' --type shape \ + --prop text="Basic Table — Per-Cell Set" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +officecli add "$PPTX" '/slide[2]' --type table \ + --prop x=0.5in --prop y=1.2in --prop width=10in --prop height=2.5in \ + --prop rows=4 --prop cols=3 --prop headerFill=2E75B6 + +# Header +for entry in "1:Product" "2:Units" "3:Revenue"; do + col="${entry%%:*}"; txt="${entry#*:}" + officecli set "$PPTX" "/slide[2]/table[1]/tr[1]/tc[$col]" \ + --prop text="$txt" --prop bold=true --prop color=FFFFFF +done + +# Body +officecli set "$PPTX" '/slide[2]/table[1]/tr[2]/tc[1]' --prop text="Widget" +officecli set "$PPTX" '/slide[2]/table[1]/tr[2]/tc[2]' --prop text="1,200" +officecli set "$PPTX" '/slide[2]/table[1]/tr[2]/tc[3]' --prop text="\$48,000" +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[1]' --prop text="Gizmo" +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[2]' --prop text="850" +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[3]' --prop text="\$72,250" +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[1]' --prop text="Sprocket" +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[2]' --prop text="430" +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[3]' --prop text="\$25,800" + +# --- Slide 3: Cell fill variations (solid hex, theme color, gradient, none) --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text="Cell Fill Variations" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +officecli add "$PPTX" '/slide[3]' --type table \ + --prop x=0.5in --prop y=1.2in --prop width=12in --prop height=4in \ + --prop rows=5 --prop cols=2 --prop style=none --prop border.all="1pt solid 808080" + +officecli set "$PPTX" '/slide[3]/table[1]/tr[1]/tc[1]' --prop text="fill spec" --prop bold=true --prop fill=404040 --prop color=FFFFFF +officecli set "$PPTX" '/slide[3]/table[1]/tr[1]/tc[2]' --prop text="rendered" --prop bold=true --prop fill=404040 --prop color=FFFFFF + +# Solid hex +officecli set "$PPTX" '/slide[3]/table[1]/tr[2]/tc[1]' --prop text='fill=FF0000 (solid hex)' +officecli set "$PPTX" '/slide[3]/table[1]/tr[2]/tc[2]' --prop fill=FF0000 + +# Named color +officecli set "$PPTX" '/slide[3]/table[1]/tr[3]/tc[1]' --prop text='fill=red / fill=rgb(255,0,0) (named / rgb forms)' +officecli set "$PPTX" '/slide[3]/table[1]/tr[3]/tc[2]' --prop fill=red + +# Theme color — accent1 follows the deck theme +officecli set "$PPTX" '/slide[3]/table[1]/tr[4]/tc[1]' --prop text='fill=accent1 (theme color, follows deck theme)' +officecli set "$PPTX" '/slide[3]/table[1]/tr[4]/tc[2]' --prop fill=accent1 + +# Gradient — "COLOR1-COLOR2[-ANGLE]" +officecli set "$PPTX" '/slide[3]/table[1]/tr[5]/tc[1]' --prop text='fill="FF0000-0000FF-90" (gradient, 90° angle)' +officecli set "$PPTX" '/slide[3]/table[1]/tr[5]/tc[2]' --prop fill="FF0000-0000FF-90" + +# fill=none demo (separate small table so 'none' is visible against page bg) +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text='fill=none (explicit no-fill; cell becomes transparent):' --prop size=14 \ + --prop x=0.5in --prop y=5.4in --prop width=12in --prop height=0.4in +officecli add "$PPTX" '/slide[3]' --type table \ + --prop x=0.5in --prop y=5.9in --prop width=4in --prop height=0.8in \ + --prop rows=1 --prop cols=2 --prop style=none --prop border.all="1pt solid 000000" +officecli set "$PPTX" '/slide[3]/table[2]/tr[1]/tc[1]' --prop text="solid" --prop fill=FFE699 +officecli set "$PPTX" '/slide[3]/table[2]/tr[1]/tc[2]' --prop text="none" --prop fill=none + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/tables/tables-borders.md b/examples/ppt/tables/tables-borders.md new file mode 100644 index 000000000..ab2e3b192 --- /dev/null +++ b/examples/ppt/tables/tables-borders.md @@ -0,0 +1,76 @@ +# PPT Table Borders + +Three files work together: + +- **tables-borders.sh** — Build script. +- **tables-borders.pptx** — 3-slide deck. +- **tables-borders.md** — This file. + +## Regenerate + +```bash +cd examples/ppt +bash tables-borders.sh +# → tables-borders.pptx +``` + +## Border format + +PPT accepts two equivalent formats. Pick whichever reads best for your case: + +| Form | Example | Notes | +|---|---|---| +| **Space-separated** | `"1pt solid FF0000"` | `"WIDTH[ DASH][ COLOR]"`, WIDTH in pt | +| **Semicolon** | `"single;4;FF0000"` | `"STYLE;WIDTH;COLOR[;DASH]"`, WIDTH in 1/8 pt — matches docx | +| **Semicolon + dash** | `"single;8;0070C0;dash"` | optional dash on the end | +| **Clear** | `"none"` | removes the border | + +`DASH ∈ solid | dot | dash | lgDash | dashDot | sysDot | sysDash`. +STYLE in the semicolon form is ignored by pptx (kept for docx compatibility). + +## Slides + +### Slide 1 — Shorthand & per-edge + +Six small tables demonstrating: + +- `border.all="1pt solid 808080"` — grey grid on every edge +- `border.all="2pt solid FF0000"` — thick red on every edge +- `border.all=none` — clear borders (table is invisible) +- `border.top` / `border.bottom` / `border.left` — outer edges only + +### Slide 2 — Inside dividers & dash patterns + +- `border.horizontal` (alias `border.insideH`) — between rows +- `border.vertical` (alias `border.insideV`) — between columns +- Dash variations: `lgDash`, `dashDot`, `sysDash` + +```bash +officecli add file.pptx /slide[2] --type table \ + --prop border.horizontal="1pt solid CCCCCC" \ + --prop border.all="1pt solid 404040" \ + --prop data="A,B,C;1,2,3;..." +``` + +### Slide 3 — Diagonal borders + +Per-cell `border.tl2br` / `border.tr2bl` for crossed-out headers, N/A cells, +or matrix corners: + +```bash +# Header corner cell with a diagonal split +officecli set file.pptx /slide[3]/table[1]/tr[1]/tc[1] \ + --prop border.tl2br="1pt solid 808080" + +# N/A cell with both diagonals (X) +officecli set file.pptx /slide[3]/table[2]/tr[1]/tc[1] \ + --prop text="N/A" \ + --prop border.tl2br="1pt solid C00000" \ + --prop border.tr2bl="1pt solid C00000" +``` + +Diagonal borders are **add/set only** — `get` does not surface them today. + +**Features:** `border.all`, `border.top/right/bottom/left`, +`border.horizontal`/`border.vertical`, `border.tl2br`/`border.tr2bl`, +dash patterns. diff --git a/examples/ppt/tables/tables-borders.pptx b/examples/ppt/tables/tables-borders.pptx new file mode 100644 index 000000000..993c4fc55 Binary files /dev/null and b/examples/ppt/tables/tables-borders.pptx differ diff --git a/examples/ppt/tables/tables-borders.sh b/examples/ppt/tables/tables-borders.sh new file mode 100644 index 000000000..bbb0b715e --- /dev/null +++ b/examples/ppt/tables/tables-borders.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# PowerPoint table border styling. +# Demonstrates: border.all shorthand, per-edge borders (top/right/bottom/left), +# inside dividers (horizontal/vertical), diagonal borders (tl2br/tr2bl), +# dash patterns (solid/dot/dash/lgDash/dashDot/sysDot/sysDash). + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/tables-borders.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +DATA="A,B,C;1,2,3;4,5,6;7,8,9" + +add_table () { + local slide="$1" x="$2" y="$3" label="$4"; shift 4 + local ty + ty=$(awk -v a="$y" 'BEGIN{print a+0.35}') + officecli add "$PPTX" "/slide[$slide]" --type shape \ + --prop text="$label" --prop size=12 --prop bold=true \ + --prop x="${x}in" --prop y="${y}in" --prop width=4in --prop height=0.3in + officecli add "$PPTX" "/slide[$slide]" --type table \ + --prop x="${x}in" --prop y="${ty}in" \ + --prop width=3.5in --prop height=1.8in --prop style=none \ + --prop data="$DATA" "$@" +} + +# --- Slide 1: border shorthand & per-edge --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="Borders: Shorthand & Per-Edge" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +add_table 1 0.5 1.0 "border.all=1pt solid 808080" --prop border.all="1pt solid 808080" +add_table 1 5.0 1.0 "border.all=2pt solid FF0000" --prop border.all="2pt solid FF0000" +add_table 1 9.5 1.0 "border.all=none" --prop border.all=none + +add_table 1 0.5 3.5 "border.top=3pt solid 000000" --prop border.top="3pt solid 000000" +add_table 1 5.0 3.5 "border.bottom=3pt solid 0070C0" --prop border.bottom="3pt solid 0070C0" +add_table 1 9.5 3.5 "border.left=3pt solid 00B050" --prop border.left="3pt solid 00B050" + +# --- Slide 2: inside dividers & dash patterns --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[2]' --type shape \ + --prop text="Borders: Inside Dividers & Dashes" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +add_table 2 0.5 1.0 "border.horizontal=1pt solid CCC" \ + --prop border.horizontal="1pt solid CCCCCC" --prop border.all="1pt solid 404040" +add_table 2 5.0 1.0 "border.vertical=1pt dash 0070C0" \ + --prop border.vertical="1pt dash 0070C0" --prop border.all="1pt solid 404040" +add_table 2 9.5 1.0 "horizontal+vertical=dot" \ + --prop border.horizontal="1pt dot 808080" --prop border.vertical="1pt dot 808080" \ + --prop border.all="2pt solid 000000" + +add_table 2 0.5 3.5 "dash=lgDash" --prop border.all="1.5pt lgDash FF0000" +add_table 2 5.0 3.5 "dash=dashDot" --prop border.all="1.5pt dashDot 0070C0" +add_table 2 9.5 3.5 "dash=sysDash" --prop border.all="1.5pt sysDash 00B050" + +# --- Slide 3: diagonal borders (per-cell) --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text="Diagonal Borders (per-cell, tl2br / tr2bl)" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text="Typical use: 'crossed out' header corner cell." --prop size=14 \ + --prop x=0.5in --prop y=0.95in --prop width=12in --prop height=0.4in + +officecli add "$PPTX" '/slide[3]' --type table \ + --prop x=2in --prop y=1.6in --prop width=9in --prop height=3in \ + --prop rows=4 --prop cols=4 --prop border.all="1pt solid 808080" + +# Top-left corner: diagonal split with 'Month' / 'Region' labels +officecli set "$PPTX" '/slide[3]/table[1]/tr[1]/tc[1]' \ + --prop text="" --prop fill=F2F2F2 \ + --prop border.tl2br="1pt solid 808080" + +# Column headers +officecli set "$PPTX" '/slide[3]/table[1]/tr[1]/tc[2]' --prop text="Jan" --prop bold=true --prop align=center --prop fill=DEEAF6 +officecli set "$PPTX" '/slide[3]/table[1]/tr[1]/tc[3]' --prop text="Feb" --prop bold=true --prop align=center --prop fill=DEEAF6 +officecli set "$PPTX" '/slide[3]/table[1]/tr[1]/tc[4]' --prop text="Mar" --prop bold=true --prop align=center --prop fill=DEEAF6 + +# Row headers + data +officecli set "$PPTX" '/slide[3]/table[1]/tr[2]/tc[1]' --prop text="North" --prop bold=true --prop fill=DEEAF6 +officecli set "$PPTX" '/slide[3]/table[1]/tr[2]/tc[2]' --prop text="120" +officecli set "$PPTX" '/slide[3]/table[1]/tr[2]/tc[3]' --prop text="135" +officecli set "$PPTX" '/slide[3]/table[1]/tr[2]/tc[4]' --prop text="142" +officecli set "$PPTX" '/slide[3]/table[1]/tr[3]/tc[1]' --prop text="South" --prop bold=true --prop fill=DEEAF6 +officecli set "$PPTX" '/slide[3]/table[1]/tr[3]/tc[2]' --prop text="98" +officecli set "$PPTX" '/slide[3]/table[1]/tr[3]/tc[3]' --prop text="110" +officecli set "$PPTX" '/slide[3]/table[1]/tr[3]/tc[4]' --prop text="121" +officecli set "$PPTX" '/slide[3]/table[1]/tr[4]/tc[1]' --prop text="East" --prop bold=true --prop fill=DEEAF6 +officecli set "$PPTX" '/slide[3]/table[1]/tr[4]/tc[2]' --prop text="165" +officecli set "$PPTX" '/slide[3]/table[1]/tr[4]/tc[3]' --prop text="178" +officecli set "$PPTX" '/slide[3]/table[1]/tr[4]/tc[4]' --prop text="190" + +# A standalone cell with both diagonals (X pattern) +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text="Both diagonals on a single cell:" --prop size=14 \ + --prop x=0.5in --prop y=5.2in --prop width=12in --prop height=0.4in +officecli add "$PPTX" '/slide[3]' --type table \ + --prop x=5in --prop y=5.7in --prop width=3in --prop height=1.2in \ + --prop rows=1 --prop cols=1 --prop border.all="1pt solid 000000" +officecli set "$PPTX" '/slide[3]/table[2]/tr[1]/tc[1]' \ + --prop text="N/A" --prop align=center --prop fill=F2F2F2 \ + --prop border.tl2br="1pt solid C00000" \ + --prop border.tr2bl="1pt solid C00000" + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/tables/tables-financial.md b/examples/ppt/tables/tables-financial.md new file mode 100644 index 000000000..cce0c55ca --- /dev/null +++ b/examples/ppt/tables/tables-financial.md @@ -0,0 +1,64 @@ +# Real-World PPT Tables — Financial Review Deck + +Three files work together: + +- **tables-financial.sh** — Build script. +- **tables-financial.pptx** — 4-slide deck (title + 3 table slides). +- **tables-financial.md** — This file. + +A realistic finance deck combining the techniques from the other +`tables-*` examples: a quarterly P&L with section headers, a risk register +with traffic-light status fills, and a KPI summary using a built-in theme +style. + +## Regenerate + +```bash +cd examples/ppt +bash tables-financial.sh +# → tables-financial.pptx +``` + +## Slides + +### Slide 1 — Title + +Plain text shapes, no table — establishes a navy/grey theme used throughout. + +### Slide 2 — Quarterly P&L + +11×6 table. Demonstrates: + +- **Header row** — solid navy fill, white bold centered text. +- **Section bands** — `gridSpan=6` cells used as REVENUE / EXPENSES dividers. +- **Subtotal emphasis** — pale-blue row fill via per-cell `fill=$PALE`. +- **Net Income highlight** — green-fill row across all columns. +- **Right-aligned numbers** — `align=right` on numeric columns; bold on totals. + +### Slide 3 — Risk Register (traffic-light fills) + +Built with `style=medium2` + `firstRow + bandedRows`, then the Status +column is overridden per-cell with green / amber / red fills: + +```bash +officecli set file.pptx /slide[3]/table[1]/tr[4]/tc[5] \ + --prop text="Critical" --prop fill=C00000 \ + --prop color=FFFFFF --prop bold=true --prop align=center +``` + +### Slide 4 — KPI Summary + +Built-in `style=medium4` with `firstRow + firstCol + lastRow` — a compact +table where every visual element (header band, first-column emphasis, +totals row) comes from the theme; no per-cell styling needed beyond the +inline `data=` seed. + +```bash +officecli add file.pptx /slide[4] --type table \ + --prop style=medium4 \ + --prop firstRow=true --prop firstCol=true --prop lastRow=true \ + --prop data="Metric,Target,Actual,Variance;Revenue (\$M),8.0,8.6,+7.5%;..." +``` + +**Features used:** all of `tables-basic`, `tables-styled`, `tables-merged`, +plus per-cell traffic-light fills. diff --git a/examples/ppt/tables/tables-financial.pptx b/examples/ppt/tables/tables-financial.pptx new file mode 100644 index 000000000..6c850bf51 Binary files /dev/null and b/examples/ppt/tables/tables-financial.pptx differ diff --git a/examples/ppt/tables/tables-financial.sh b/examples/ppt/tables/tables-financial.sh new file mode 100644 index 000000000..c31cb7c8e --- /dev/null +++ b/examples/ppt/tables/tables-financial.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Real-world PowerPoint table example — quarterly financial report deck. +# Combines: built-in style, header banding, per-cell fills for traffic-light +# status, gridSpan section headers, right-aligned numbers, totals row. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/tables-financial.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +# Theme colors +NAVY=1F3864; STEEL=2E75B6; PALE=DEEAF6 +GREEN=00B050; AMBER=FFC000; RED=C00000 + +# --- Slide 1: Title --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="Q4 2025 Financial Review" --prop size=44 --prop bold=true --prop color="$NAVY" \ + --prop x=1in --prop y=2.5in --prop width=11in --prop height=1.2in --prop align=center +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="Revenue · Expenses · Margin · Forecast" --prop size=22 --prop color=595959 \ + --prop x=1in --prop y=4in --prop width=11in --prop height=0.8in --prop align=center + +# --- Slide 2: Quarterly P&L (sections via gridSpan) --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[2]' --type shape \ + --prop text="Quarterly P&L (USD, thousands)" --prop size=28 --prop bold=true --prop color="$NAVY" \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +officecli add "$PPTX" '/slide[2]' --type table \ + --prop x=0.5in --prop y=1.2in --prop width=12in --prop height=5.5in \ + --prop rows=11 --prop cols=6 + +# Header +for entry in "1:Line Item" "2:Q1" "3:Q2" "4:Q3" "5:Q4" "6:FY Total"; do + c="${entry%%:*}"; t="${entry#*:}" + officecli set "$PPTX" "/slide[2]/table[1]/tr[1]/tc[$c]" \ + --prop text="$t" --prop bold=true --prop color=FFFFFF --prop fill="$NAVY" \ + --prop align=center +done + +# Section: Revenue +officecli set "$PPTX" '/slide[2]/table[1]/tr[2]/tc[1]' \ + --prop text="REVENUE" --prop bold=true --prop fill="$STEEL" --prop color=FFFFFF \ + --prop gridSpan=6 + +set_row () { + local r="$1" label="$2" q1="$3" q2="$4" q3="$5" q4="$6" tot="$7" emphasize="$8" + local fill="" + [ "$emphasize" = "1" ] && fill="--prop fill=$PALE" + officecli set "$PPTX" "/slide[2]/table[1]/tr[$r]/tc[1]" --prop text="$label" $fill + officecli set "$PPTX" "/slide[2]/table[1]/tr[$r]/tc[2]" --prop text="$q1" --prop align=right $fill + officecli set "$PPTX" "/slide[2]/table[1]/tr[$r]/tc[3]" --prop text="$q2" --prop align=right $fill + officecli set "$PPTX" "/slide[2]/table[1]/tr[$r]/tc[4]" --prop text="$q3" --prop align=right $fill + officecli set "$PPTX" "/slide[2]/table[1]/tr[$r]/tc[5]" --prop text="$q4" --prop align=right $fill + officecli set "$PPTX" "/slide[2]/table[1]/tr[$r]/tc[6]" --prop text="$tot" --prop align=right --prop bold=true $fill +} + +set_row 3 " Product Sales" "1,200" "1,350" "1,480" "1,720" "5,750" 0 +set_row 4 " Services" "480" "520" "590" "640" "2,230" 0 +set_row 5 " Licensing" "120" "140" "165" "195" "620" 0 +set_row 6 " Subtotal" "1,800" "2,010" "2,235" "2,555" "8,600" 1 + +# Section: Expenses +officecli set "$PPTX" '/slide[2]/table[1]/tr[7]/tc[1]' \ + --prop text="EXPENSES" --prop bold=true --prop fill="$STEEL" --prop color=FFFFFF \ + --prop gridSpan=6 + +set_row 8 " COGS" "720" "810" "895" "1,025" "3,450" 0 +set_row 9 " Operating" "380" "410" "445" "490" "1,725" 0 +set_row 10 " Subtotal" "1,100" "1,220" "1,340" "1,515" "5,175" 1 + +# Net row +officecli set "$PPTX" '/slide[2]/table[1]/tr[11]/tc[1]' \ + --prop text="NET INCOME" --prop bold=true --prop fill="$GREEN" --prop color=FFFFFF +for entry in "2:700" "3:790" "4:895" "5:1,040" "6:3,425"; do + c="${entry%%:*}"; v="${entry#*:}" + officecli set "$PPTX" "/slide[2]/table[1]/tr[11]/tc[$c]" \ + --prop text="$v" --prop align=right --prop bold=true \ + --prop fill="$GREEN" --prop color=FFFFFF +done + +# --- Slide 3: Risk register (traffic-light fills) --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text="Risk Register" --prop size=28 --prop bold=true --prop color="$NAVY" \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +officecli add "$PPTX" '/slide[3]' --type table \ + --prop x=0.5in --prop y=1.2in --prop width=12in --prop height=4in \ + --prop style=medium2 --prop firstRow=true --prop bandedRows=true \ + --prop data="Risk,Impact,Likelihood,Owner,Status;FX volatility,High,Medium,CFO,At risk;Supply chain,Medium,Low,COO,On track;Talent attrition,High,High,CPO,Critical;Reg compliance,Medium,Medium,GC,On track;Cybersecurity,High,Low,CTO,On track" + +# Color the Status column (col 5, rows 2..6) +officecli set "$PPTX" '/slide[3]/table[1]/tr[2]/tc[5]' \ + --prop text="At risk" --prop fill="$AMBER" --prop bold=true --prop align=center +officecli set "$PPTX" '/slide[3]/table[1]/tr[3]/tc[5]' \ + --prop text="On track" --prop fill="$GREEN" --prop color=FFFFFF --prop bold=true --prop align=center +officecli set "$PPTX" '/slide[3]/table[1]/tr[4]/tc[5]' \ + --prop text="Critical" --prop fill="$RED" --prop color=FFFFFF --prop bold=true --prop align=center +officecli set "$PPTX" '/slide[3]/table[1]/tr[5]/tc[5]' \ + --prop text="On track" --prop fill="$GREEN" --prop color=FFFFFF --prop bold=true --prop align=center +officecli set "$PPTX" '/slide[3]/table[1]/tr[6]/tc[5]' \ + --prop text="On track" --prop fill="$GREEN" --prop color=FFFFFF --prop bold=true --prop align=center + +# --- Slide 4: KPI summary (small table) --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[4]' --type shape \ + --prop text="KPI Summary" --prop size=28 --prop bold=true --prop color="$NAVY" \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +officecli add "$PPTX" '/slide[4]' --type table \ + --prop x=2in --prop y=1.5in --prop width=9in --prop height=3.5in \ + --prop style=medium4 --prop firstRow=true --prop firstCol=true --prop lastRow=true \ + --prop data="Metric,Target,Actual,Variance;Revenue (\$M),8.0,8.6,+7.5%;Gross Margin,38%,40.1%,+2.1pp;Op Margin,18%,19.8%,+1.8pp;CAC Payback,14 mo,12 mo,-2 mo;Total,—,—,Beat" + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/tables/tables-merged.md b/examples/ppt/tables/tables-merged.md new file mode 100644 index 000000000..31a6d2431 --- /dev/null +++ b/examples/ppt/tables/tables-merged.md @@ -0,0 +1,66 @@ +# Merged Cells in PPT Tables + +Three files work together: + +- **tables-merged.sh** — Build script. +- **tables-merged.pptx** — 2-slide deck. +- **tables-merged.md** — This file. + +## Regenerate + +```bash +cd examples/ppt +bash tables-merged.sh +# → tables-merged.pptx +``` + +## Merge axes — horizontal + vertical + +PPT OOXML supports horizontal (`gridSpan` + `hMerge`) and vertical +(`rowSpan` + `vMerge`) merging. officecli exposes both on the write side: + +- `gridSpan=N` on a cell — horizontal merge across N columns +- `merge.down=N` on a cell — vertical merge spanning N+1 rows total + (anchor + N continuation rows below) + +This file walks through `gridSpan` (the more common case). See +`tables-rows-cols.{md,sh}` slide 4 for a `merge.down` example. + +## Slides + +### Slide 1 — Two-level header (`gridSpan` on the super-header row) + +A super-header row where two cells each span two columns: + +``` +| Department | 2024 Performance | 2025 Forecast | +| | Revenue | Margin | Revenue | Margin | +| Eng | 1.20M | 18% | 1.45M | 22% | +``` + +```bash +# Row 1: super-headers. gridSpan=2 stamps hMerge on the next cell. +officecli set file.pptx /slide[1]/table[1]/tr[1]/tc[2] \ + --prop text="2024 Performance" --prop gridSpan=2 + +# tc[3] is now a continuation cell — skip to tc[4]: +officecli set file.pptx /slide[1]/table[1]/tr[1]/tc[4] \ + --prop text="2025 Forecast" --prop gridSpan=2 +``` + +**Important:** cell indices do **not** renumber after `gridSpan`. The merged +cells still occupy their original `tc[N]` slots; you just shouldn't set text +on them. Setting `gridSpan=2` on `tc[2]` doesn't make `tc[3]` go away — it +flags `tc[3]` as `hMerge=true`. + +### Slide 2 — Full-width section headers + +Span the entire table to create section dividers, then list items below: + +```bash +officecli set file.pptx /slide[2]/table[1]/tr[2]/tc[1] \ + --prop text="◆ Phase 1 — Discovery" --prop bold=true \ + --prop fill=FFE699 --prop gridSpan=4 +``` + +**Features:** `gridSpan`, per-cell `fill` for color-coding sections. diff --git a/examples/ppt/tables/tables-merged.pptx b/examples/ppt/tables/tables-merged.pptx new file mode 100644 index 000000000..80781e1f1 Binary files /dev/null and b/examples/ppt/tables/tables-merged.pptx differ diff --git a/examples/ppt/tables/tables-merged.sh b/examples/ppt/tables/tables-merged.sh new file mode 100644 index 000000000..8ce401df2 --- /dev/null +++ b/examples/ppt/tables/tables-merged.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# PowerPoint table cell merging — horizontal merge via gridSpan. +# Demonstrates: multi-column header spans, section headers spanning the table, +# nested header hierarchy. +# +# Note: officecli supports both horizontal (gridSpan) and vertical (merge.down) +# write-side merging. This file walks through gridSpan; see tables-rows-cols.sh +# slide 4 for a merge.down example. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/tables-merged.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +# --- Slide 1: 2-level header (gridSpan on row 1) --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="Two-Level Header (gridSpan)" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +officecli add "$PPTX" '/slide[1]' --type table \ + --prop x=0.5in --prop y=1.2in --prop width=12in --prop height=3.5in \ + --prop rows=6 --prop cols=5 --prop headerFill=2E75B6 --prop bodyFill=DEEAF6 + +# Row 1: super-headers +officecli set "$PPTX" '/slide[1]/table[1]/tr[1]/tc[1]' \ + --prop text="Department" --prop bold=true --prop color=FFFFFF --prop align=center +officecli set "$PPTX" '/slide[1]/table[1]/tr[1]/tc[2]' \ + --prop text="2024 Performance" --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop gridSpan=2 +# tc[3] is now a continuation cell from gridSpan=2 — skip directly to tc[4]. +officecli set "$PPTX" '/slide[1]/table[1]/tr[1]/tc[4]' \ + --prop text="2025 Forecast" --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop gridSpan=2 + +# Row 2: sub-headers (lighter shade) +officecli set "$PPTX" '/slide[1]/table[1]/tr[2]/tc[1]' \ + --prop text="" --prop fill=5B9BD5 +officecli set "$PPTX" '/slide[1]/table[1]/tr[2]/tc[2]' \ + --prop text="Revenue" --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=5B9BD5 +officecli set "$PPTX" '/slide[1]/table[1]/tr[2]/tc[3]' \ + --prop text="Margin" --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=5B9BD5 +officecli set "$PPTX" '/slide[1]/table[1]/tr[2]/tc[4]' \ + --prop text="Revenue" --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=5B9BD5 +officecli set "$PPTX" '/slide[1]/table[1]/tr[2]/tc[5]' \ + --prop text="Margin" --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=5B9BD5 + +# Body rows +for row in "3:Engineering:1.20M:18%:1.45M:22%" \ + "4:Sales:2.30M:12%:2.80M:15%" \ + "5:Marketing:0.95M:25%:1.10M:28%" \ + "6:Operations:0.78M:30%:0.85M:32%"; do + IFS=':' read -r r d a b c e <<< "$row" + officecli set "$PPTX" "/slide[1]/table[1]/tr[$r]/tc[1]" --prop text="$d" --prop bold=true + officecli set "$PPTX" "/slide[1]/table[1]/tr[$r]/tc[2]" --prop text="$a" --prop align=right + officecli set "$PPTX" "/slide[1]/table[1]/tr[$r]/tc[3]" --prop text="$b" --prop align=right + officecli set "$PPTX" "/slide[1]/table[1]/tr[$r]/tc[4]" --prop text="$c" --prop align=right + officecli set "$PPTX" "/slide[1]/table[1]/tr[$r]/tc[5]" --prop text="$e" --prop align=right +done + +# --- Slide 2: Section header rows spanning the full table --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[2]' --type shape \ + --prop text="Full-Width Section Headers" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +officecli add "$PPTX" '/slide[2]' --type table \ + --prop x=0.5in --prop y=1.2in --prop width=12in --prop height=4.5in \ + --prop rows=9 --prop cols=4 --prop headerFill=1F3864 + +# Header +for entry in "1:Item" "2:Owner" "3:Due" "4:Status"; do + c="${entry%%:*}"; t="${entry#*:}" + officecli set "$PPTX" "/slide[2]/table[1]/tr[1]/tc[$c]" \ + --prop text="$t" --prop bold=true --prop color=FFFFFF +done + +# Section: "Phase 1" — spans all 4 columns +officecli set "$PPTX" '/slide[2]/table[1]/tr[2]/tc[1]' \ + --prop text="◆ Phase 1 — Discovery" --prop bold=true --prop fill=FFE699 \ + --prop gridSpan=4 +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[1]' --prop text="Stakeholder interviews" +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[2]' --prop text="Alice" +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[3]' --prop text="Mar 15" +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[4]' --prop text="✓ Done" --prop color=00B050 +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[1]' --prop text="Market research" +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[2]' --prop text="Bob" +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[3]' --prop text="Mar 30" +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[4]' --prop text="✓ Done" --prop color=00B050 + +# Section: "Phase 2" +officecli set "$PPTX" '/slide[2]/table[1]/tr[5]/tc[1]' \ + --prop text="◆ Phase 2 — Design" --prop bold=true --prop fill=C6E0B4 \ + --prop gridSpan=4 +officecli set "$PPTX" '/slide[2]/table[1]/tr[6]/tc[1]' --prop text="Architecture spec" +officecli set "$PPTX" '/slide[2]/table[1]/tr[6]/tc[2]' --prop text="Carol" +officecli set "$PPTX" '/slide[2]/table[1]/tr[6]/tc[3]' --prop text="Apr 20" +officecli set "$PPTX" '/slide[2]/table[1]/tr[6]/tc[4]' --prop text="◐ WIP" --prop color=FFC000 + +# Section: "Phase 3" +officecli set "$PPTX" '/slide[2]/table[1]/tr[7]/tc[1]' \ + --prop text="◆ Phase 3 — Build" --prop bold=true --prop fill=F4B084 \ + --prop gridSpan=4 +officecli set "$PPTX" '/slide[2]/table[1]/tr[8]/tc[1]' --prop text="Backend services" +officecli set "$PPTX" '/slide[2]/table[1]/tr[8]/tc[2]' --prop text="Dave" +officecli set "$PPTX" '/slide[2]/table[1]/tr[8]/tc[3]' --prop text="Jun 15" +officecli set "$PPTX" '/slide[2]/table[1]/tr[8]/tc[4]' --prop text="◯ Not started" +officecli set "$PPTX" '/slide[2]/table[1]/tr[9]/tc[1]' --prop text="Frontend UI" +officecli set "$PPTX" '/slide[2]/table[1]/tr[9]/tc[2]' --prop text="Eve" +officecli set "$PPTX" '/slide[2]/table[1]/tr[9]/tc[3]' --prop text="Jul 01" +officecli set "$PPTX" '/slide[2]/table[1]/tr[9]/tc[4]' --prop text="◯ Not started" + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/tables/tables-rows-cols.md b/examples/ppt/tables/tables-rows-cols.md new file mode 100644 index 000000000..6255a3a2e --- /dev/null +++ b/examples/ppt/tables/tables-rows-cols.md @@ -0,0 +1,147 @@ +# PPT Table Rows & Columns + +Three files work together: + +- **tables-rows-cols.sh** — Build script. +- **tables-rows-cols.pptx** — 4-slide deck. +- **tables-rows-cols.md** — This file. + +Covers the `row` and `column` child elements of `table`, which the other +`tables-*` examples leave as one-line static `rows`/`cols` props. + +## Regenerate + +```bash +cd examples/ppt +bash tables-rows-cols.sh +# → tables-rows-cols.pptx +``` + +## Slides + +### Slide 1 — Grow an existing table (theme vs per-cell stamp) + +This slide shows **two tables side by side** that are populated identically +but colored two different ways — the contrast is the point of the slide. + +```bash +# Append a row. Inherits column count; seed cells via c{N}=value. +officecli add file.pptx /slide[1]/table[1] --type row \ + --prop c1=Bob --prop c2=95 --prop c3=110 + +# Append a column. Inserts a cell in EVERY existing row. +officecli add file.pptx /slide[1]/table[1] --type column \ + --prop width=2cm --prop text="Q3" +``` + +`text=` on `add column` seeds the same value into every cell of the new +column (useful for adding a placeholder "—" column). Per-row body values +go in via `set` on the newly created `tc[N]` cells. + +### The two coloring models — pick one deliberately + +PowerPoint tables (and Word tables, and Excel tables) have **two +independent ways** to color cells. They behave differently when you +later `add row` / `add column`: + +| Model | How you write it | Stored as | Appended row/col follows? | +|---|---|---|---| +| **Theme (recommended for "auto-follow")** | `--prop style=medium2` (+ `firstRow`/`bandedRows`/…) | Table-level `` reference. Renderer paints all cells in range, including ones added later. | ✓ **Yes — automatic.** Same model as Excel Table styles. | +| **Per-cell stamp** | `--prop headerFill=4472C4 --prop bodyFill=DEEAF6` *(or any later `set tc[N] fill=…`)* | `` fanned out onto **each existing cell** at that moment. | ✗ **No — never.** Per-cell fills are personal overrides; they don't spread. | + +This is not a bug or a gap — it's the OOXML model speaking through +officecli. The same separation exists for `` vs cell +`` in PPT, `` vs cell `` in Word, and +`` vs cell-level fills in Excel. + +**Rule of thumb:** + +- Want appended rows/columns to look the same as the original? → use a + **theme style** (`style=medium2|light1|dark1|…`). +- Need a specific custom color that's not in any theme? → use + `headerFill`/`bodyFill` (or per-cell `fill=`), and accept that you'll + manually fill new cells after `add row`/`add column`. Table B on + slide 1 of the demo deck shows exactly this top-up: + + ```bash + HDR=4472C4; BODY=DEEAF6 + # Total header added via 'add column' — needs headerFill manually + officecli set file.pptx /slide[1]/table[2]/tr[1]/tc[4] \ + --prop fill=$HDR --prop color=FFFFFF --prop bold=true + + # Newly appended Bob/Carol rows — need bodyFill on each cell + for c in 1 2 3 4; do + officecli set file.pptx /slide[1]/table[2]/tr[3]/tc[$c] --prop fill=$BODY + officecli set file.pptx /slide[1]/table[2]/tr[4]/tc[$c] --prop fill=$BODY + done + ``` + +### Slide 2 — Per-row height + per-column width + +After a table is created, each row and column can have its own size: + +```bash +# Custom column widths (must sum to roughly the table width) +officecli set file.pptx /slide[2]/table[1]/col[1] --prop width=2in +officecli set file.pptx /slide[2]/table[1]/col[2] --prop width=1.5in +officecli set file.pptx /slide[2]/table[1]/col[3] --prop width=7in +officecli set file.pptx /slide[2]/table[1]/col[4] --prop width=1.5in + +# Custom row heights — header thin, body increasing +officecli set file.pptx /slide[2]/table[1]/tr[1] --prop height=0.5in +officecli set file.pptx /slide[2]/table[1]/tr[2] --prop height=0.6in +officecli set file.pptx /slide[2]/table[1]/tr[3] --prop height=1in +officecli set file.pptx /slide[2]/table[1]/tr[4] --prop height=1.5in +``` + +### Slide 3 — Uniform `rowHeight` (table-level) + +When every row should be the same height, set `rowHeight` once at +`add table` time instead of running `set tr[N] height=` N times: + +```bash +officecli add file.pptx /slide[3] --type table \ + --prop rows=5 --prop cols=3 --prop rowHeight=0.8in \ + --prop data="Step,Action,Result;1,Init,OK;..." +``` + +### Slide 4 — Cell merging: `gridSpan` (horizontal) + `merge.down` (vertical) + +OOXML fixes a table's column count at `` and row count at +`` — no "narrower row" or "shorter column" exists. Visual merging +is done in-place on the full grid via `gridSpan` (horizontal) or +`merge.down` (vertical, wraps `rowSpan` + `vMerge` continuation cells). + +**Top table — `gridSpan=N` (full-width footnote):** + +```bash +# Append a normal 4-cell row, then horizontally merge tc[1] across all 4 cols. +officecli add file.pptx /slide[4]/table[1] --type row \ + --prop c1="Footnote: figures in thousands USD, unaudited." +officecli set file.pptx /slide[4]/table[1]/tr[3]/tc[1] \ + --prop gridSpan=4 --prop fill=F2F2F2 --prop bold=true +``` + +`gridSpan=N` on `tc[1]` flags the next N-1 cells as `hMerge=true` — they +keep their `tc[N]` slots but render as part of the wide cell. Don't set +text on the continuation cells. + +**Bottom table — `merge.down=N` (grouped row labels):** + +```bash +# Merge "North" cell down 3 rows total (anchor + 2 continuations). +officecli set file.pptx /slide[4]/table[2]/tr[2]/tc[1] \ + --prop merge.down=2 --prop bold=true --prop fill=DEEAF6 --prop valign=middle +``` + +`merge.down=N` sets `rowSpan=N+1` on the anchor cell and `vMerge=true` +on the N continuation cells directly below. Useful for grouped row +labels (region/category bands). + +**Rule of thumb:** for full-width headers / footnotes / totals, use +`gridSpan` on a normal row. For grouped row labels spanning vertically, +use `merge.down`. + +**Features:** `add row`, `add column`, `set row.height`, `set col.width`, +table-level `rowHeight`, `c{N}=value` cell seeding, column `text=` seed, +`gridSpan` (horizontal merge), `merge.down` (vertical merge). diff --git a/examples/ppt/tables/tables-rows-cols.pptx b/examples/ppt/tables/tables-rows-cols.pptx new file mode 100644 index 000000000..59313dfdf Binary files /dev/null and b/examples/ppt/tables/tables-rows-cols.pptx differ diff --git a/examples/ppt/tables/tables-rows-cols.sh b/examples/ppt/tables/tables-rows-cols.sh new file mode 100644 index 000000000..e3b48fdd2 --- /dev/null +++ b/examples/ppt/tables/tables-rows-cols.sh @@ -0,0 +1,204 @@ +#!/bin/bash +# PowerPoint table row & column operations. +# Demonstrates: add row / add column (grow an existing table), +# per-row height (set row.height), per-column width (set col.width), +# column seed text, gridSpan (horizontal merge), merge.down (vertical merge). + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/tables-rows-cols.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +# --- Slide 1: Grow a table by add row / add column --- +# Two side-by-side tables compare the two coloring models: +# LEFT A. style=medium2 → table-level theme, auto-follows new rows/columns. +# RIGHT B. headerFill/bodyFill → per-cell stamp, does NOT follow; manual top-up needed. +# Placed side-by-side (each 6in wide, half the 13.33in widescreen slide) so the +# visual contrast is immediate. +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="Grow a Table — Theme vs Per-Cell Stamp" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# === LEFT: Table A — style=medium2 (theme, auto-inherits) === +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="A) style=medium2 (auto-follows)" --prop size=14 --prop bold=true \ + --prop x=0.5in --prop y=1in --prop width=6in --prop height=0.4in + +officecli add "$PPTX" '/slide[1]' --type table \ + --prop x=0.5in --prop y=1.5in --prop width=2in --prop height=1.5in \ + --prop style=medium2 --prop firstRow=true --prop bandedRows=true --prop lastCol=true \ + --prop data="Name,H1;Alice,220" + +# Append 2 rows + 1 column — set NOTHING about fill. PowerPoint paints +# every new cell via the medium2 theme. H1 = first-half total (Q1+Q2), +# H2 = second-half total (Q3+Q4); appended as a derived summary column. +officecli add "$PPTX" '/slide[1]/table[1]' --type row --prop c1=Bob --prop c2=205 +officecli add "$PPTX" '/slide[1]/table[1]' --type row --prop c1=Carol --prop c2=275 +officecli add "$PPTX" '/slide[1]/table[1]' --type column \ + --prop width=1in --prop text="H2" +officecli set "$PPTX" '/slide[1]/table[1]/tr[2]/tc[3]' --prop text="245" +officecli set "$PPTX" '/slide[1]/table[1]/tr[3]/tc[3]' --prop text="225" +officecli set "$PPTX" '/slide[1]/table[1]/tr[4]/tc[3]' --prop text="335" +officecli add "$PPTX" '/slide[1]/table[1]' --type column \ + --prop width=1in --prop text="Total" +officecli set "$PPTX" '/slide[1]/table[1]/tr[2]/tc[4]' --prop text="465" --prop bold=true +officecli set "$PPTX" '/slide[1]/table[1]/tr[3]/tc[4]' --prop text="430" --prop bold=true +officecli set "$PPTX" '/slide[1]/table[1]/tr[4]/tc[4]' --prop text="610" --prop bold=true + +# === RIGHT: Table B — headerFill/bodyFill (per-cell stamp, does NOT inherit) === +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="B) headerFill/bodyFill (manual top-up)" --prop size=14 --prop bold=true \ + --prop x=7in --prop y=1in --prop width=6in --prop height=0.4in + +officecli add "$PPTX" '/slide[1]' --type table \ + --prop x=7in --prop y=1.5in --prop width=2in --prop height=1.5in \ + --prop headerFill=4472C4 --prop bodyFill=DEEAF6 \ + --prop data="Name,H1;Alice,220" +officecli add "$PPTX" '/slide[1]/table[2]' --type row --prop c1=Bob --prop c2=205 +officecli add "$PPTX" '/slide[1]/table[2]' --type row --prop c1=Carol --prop c2=275 +officecli add "$PPTX" '/slide[1]/table[2]' --type column \ + --prop width=1in --prop text="H2" +officecli set "$PPTX" '/slide[1]/table[2]/tr[2]/tc[3]' --prop text="245" +officecli set "$PPTX" '/slide[1]/table[2]/tr[3]/tc[3]' --prop text="225" +officecli set "$PPTX" '/slide[1]/table[2]/tr[4]/tc[3]' --prop text="335" +officecli add "$PPTX" '/slide[1]/table[2]' --type column \ + --prop width=1in --prop text="Total" +officecli set "$PPTX" '/slide[1]/table[2]/tr[2]/tc[4]' --prop text="465" +officecli set "$PPTX" '/slide[1]/table[2]/tr[3]/tc[4]' --prop text="430" +officecli set "$PPTX" '/slide[1]/table[2]/tr[4]/tc[4]' --prop text="610" + +# Manual top-up — headerFill/bodyFill are a one-shot stamp at add-table time. +# Every cell created later by add row / add column has no fill and must be +# styled explicitly. The Total column gets a darker fill (SUM) + bold so it +# reads as a totals band; table A gets the equivalent emphasis from medium2's +# last-column theme styling for free. +HDR=4472C4; BODY=DEEAF6; SUM=B4C7E7 +# Bob, Carol body fill across the original 2 columns. +for c in 1 2; do + officecli set "$PPTX" "/slide[1]/table[2]/tr[3]/tc[$c]" --prop fill=$BODY + officecli set "$PPTX" "/slide[1]/table[2]/tr[4]/tc[$c]" --prop fill=$BODY +done +# H2 column — header HDR, body BODY for all 3 data rows. +officecli set "$PPTX" '/slide[1]/table[2]/tr[1]/tc[3]' --prop fill=$HDR --prop color=FFFFFF --prop bold=true +for r in 2 3 4; do + officecli set "$PPTX" "/slide[1]/table[2]/tr[$r]/tc[3]" --prop fill=$BODY +done +# Total column — header HDR, body SUM (bold) for all 3 data rows. +officecli set "$PPTX" '/slide[1]/table[2]/tr[1]/tc[4]' --prop fill=$HDR --prop color=FFFFFF --prop bold=true +for r in 2 3 4; do + officecli set "$PPTX" "/slide[1]/table[2]/tr[$r]/tc[4]" --prop fill=$SUM --prop bold=true +done + +# --- Slide 2: Per-row heights & per-column widths --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[2]' --type shape \ + --prop text="Per-Row Height + Per-Column Width" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +officecli add "$PPTX" '/slide[2]' --type table \ + --prop x=0.5in --prop y=1.2in --prop width=12in --prop height=4in \ + --prop rows=4 --prop cols=4 --prop headerFill=2E75B6 + +# Header +for c in 1 2 3 4; do + officecli set "$PPTX" "/slide[2]/table[1]/tr[1]/tc[$c]" \ + --prop bold=true --prop color=FFFFFF --prop align=center +done +officecli set "$PPTX" '/slide[2]/table[1]/tr[1]/tc[1]' --prop text="Field" --prop bold=true --prop color=FFFFFF +officecli set "$PPTX" '/slide[2]/table[1]/tr[1]/tc[2]' --prop text="Short" --prop bold=true --prop color=FFFFFF +officecli set "$PPTX" '/slide[2]/table[1]/tr[1]/tc[3]' --prop text="Wide" --prop bold=true --prop color=FFFFFF +officecli set "$PPTX" '/slide[2]/table[1]/tr[1]/tc[4]' --prop text="Narrow" --prop bold=true --prop color=FFFFFF + +# Custom per-column widths (the four columns total ~12in). +officecli set "$PPTX" '/slide[2]/table[1]/col[1]' --prop width=2in +officecli set "$PPTX" '/slide[2]/table[1]/col[2]' --prop width=1.5in +officecli set "$PPTX" '/slide[2]/table[1]/col[3]' --prop width=7in +officecli set "$PPTX" '/slide[2]/table[1]/col[4]' --prop width=1.5in + +# Custom per-row heights — header thin, body increasing. +officecli set "$PPTX" '/slide[2]/table[1]/tr[1]' --prop height=0.5in +officecli set "$PPTX" '/slide[2]/table[1]/tr[2]' --prop height=0.6in +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]' --prop height=1in +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]' --prop height=1.5in + +officecli set "$PPTX" '/slide[2]/table[1]/tr[2]/tc[1]' --prop text="Title" +officecli set "$PPTX" '/slide[2]/table[1]/tr[2]/tc[2]' --prop text="A" +officecli set "$PPTX" '/slide[2]/table[1]/tr[2]/tc[3]' --prop text="Standard row height (0.6in)" +officecli set "$PPTX" '/slide[2]/table[1]/tr[2]/tc[4]' --prop text="x" + +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[1]' --prop text="Body" +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[2]' --prop text="B" +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[3]' --prop text="Taller row (1in) for emphasis" +officecli set "$PPTX" '/slide[2]/table[1]/tr[3]/tc[4]' --prop text="y" + +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[1]' --prop text="Notes" +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[2]' --prop text="C" +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[3]' --prop text="Tallest row (1.5in) — multi-line content" +officecli set "$PPTX" '/slide[2]/table[1]/tr[4]/tc[4]' --prop text="z" + +# --- Slide 3: Uniform row height via table-level rowHeight --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text="Uniform rowHeight (table-level)" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Setting rowHeight at add-time stamps every row with the same height +# (no need to set each row individually). +officecli add "$PPTX" '/slide[3]' --type table \ + --prop x=0.5in --prop y=1.2in --prop width=12in \ + --prop rows=5 --prop cols=3 --prop rowHeight=0.8in \ + --prop headerFill=1F4E79 --prop bodyFill=F2F2F2 \ + --prop data="Step,Action,Result;1,Init,OK;2,Process,OK;3,Verify,OK;4,Commit,OK" + +# --- Slide 4: Cell merging — gridSpan (horizontal) + vMerge (vertical) --- +# OOXML's table model fixes row width at column count and row +# count at count — no "narrower row" or "shorter column" exists. +# Visual merging is done in-place via gridSpan (horizontal) or merge.down +# (vertical, wraps rowSpan + vMerge). Two tables on this slide show the +# two axes of merging. +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[4]' --type shape \ + --prop text="Cell Merging — gridSpan (horizontal) + merge.down (vertical)" \ + --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=1in + +# === Top table: gridSpan=N — full-width footnote === +officecli add "$PPTX" '/slide[4]' --type shape \ + --prop text="1) gridSpan=N on first cell of a row — one wide cell across all N columns" \ + --prop size=14 --prop bold=true \ + --prop x=0.5in --prop y=1in --prop width=12in --prop height=0.4in +officecli add "$PPTX" '/slide[4]' --type table \ + --prop x=0.5in --prop y=1.5in --prop width=12in --prop height=1.5in \ + --prop headerFill=2E75B6 \ + --prop data="Q1,Q2,Q3,Q4;100,120,135,150" +# Append a normal 4-cell row, then horizontally merge via gridSpan on tc[1]. +officecli add "$PPTX" '/slide[4]/table[1]' --type row \ + --prop c1="Footnote: figures in thousands USD, unaudited." +officecli set "$PPTX" '/slide[4]/table[1]/tr[3]/tc[1]' \ + --prop gridSpan=4 --prop fill=F2F2F2 --prop bold=true + +# === Bottom table: merge.down=N — grouped row labels === +officecli add "$PPTX" '/slide[4]' --type shape \ + --prop text="2) merge.down=N on a cell — one tall cell spanning N rows (vMerge + rowSpan)" \ + --prop size=14 --prop bold=true \ + --prop x=0.5in --prop y=3.3in --prop width=12in --prop height=0.4in +officecli add "$PPTX" '/slide[4]' --type table \ + --prop x=0.5in --prop y=3.8in --prop width=12in --prop height=3in \ + --prop headerFill=2E75B6 --prop rowHeight=0.5in \ + --prop data="Region,Month,Sales,Notes;North,Jan,120,;North,Feb,135,;North,Mar,142,;South,Jan,98,;South,Feb,110," +# Merge "North" cell down 3 rows (rows 2..4); merge "South" cell down 2 rows +# (rows 5..6). merge.down=N spans the cell over N+1 rows total — the anchor +# row plus N continuation rows. +officecli set "$PPTX" '/slide[4]/table[2]/tr[2]/tc[1]' \ + --prop merge.down=2 --prop bold=true --prop fill=DEEAF6 --prop valign=middle +officecli set "$PPTX" '/slide[4]/table[2]/tr[5]/tc[1]' \ + --prop merge.down=1 --prop bold=true --prop fill=DEEAF6 --prop valign=middle + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/tables/tables-styled.md b/examples/ppt/tables/tables-styled.md new file mode 100644 index 000000000..a34391efc --- /dev/null +++ b/examples/ppt/tables/tables-styled.md @@ -0,0 +1,75 @@ +# Styled PPT Tables (Built-in Styles & Banding) + +Three files work together: + +- **tables-styled.sh** — Build script. +- **tables-styled.pptx** — 11-slide deck (9 styles + 1 banding combo slide + 1 `rowHeight`/`name=` slide). +- **tables-styled.md** — This file. + +## Regenerate + +```bash +cd examples/ppt +bash tables-styled.sh +# → tables-styled.pptx +``` + +## Slides + +### Slides 1–9 — Each built-in style + +PowerPoint ships 9 named theme styles. One slide per style: + +| Slide | `--prop style=` | +|------:|-----------------| +| 1 | `medium1` | +| 2 | `medium2` (default) | +| 3 | `medium3` | +| 4 | `medium4` | +| 5 | `light1` | +| 6 | `light2` | +| 7 | `light3` | +| 8 | `dark1` | +| 9 | `dark2` | + +```bash +officecli add file.pptx /slide[1] --type table \ + --prop style=medium2 \ + --prop firstRow=true --prop bandedRows=true \ + --prop data="Region,Q1,Q2,Q3,Q4;North,120,135,142,168;..." +``` + +### Slide 10 — Banding flag combinations + +Four tables on one slide showing how the banding flags interact with a style: + +- `firstRow=true --prop bandedRows=true` — emphasized header + zebra rows +- `firstCol=true --prop bandedCols=true` — emphasized first column + zebra columns +- `firstRow=true --prop lastRow=true` — emphasized header *and* totals row +- `style=none` — no theme; pair with explicit `border.all` for visible grid + +### Slide 11 — `rowHeight` + `name=` addressing + +Two table-level props for ergonomics: + +- `rowHeight=1cm` — stamps every row with the same height at create time + (otherwise officecli derives row height from `height / rows`). +- `name=SalesData` — sets the table's `NonVisualDrawingProperties Name`. + After creation, the table can be addressed by name instead of by + positional index, which survives slide reordering: + +```bash +officecli add file.pptx /slide[11] --type table \ + --prop name=SalesData --prop rowHeight=1cm \ + --prop data="..." + +# Stable path — works even if more tables are added before this one. +officecli set file.pptx '/slide[11]/table[@name=SalesData]/tr[2]/tc[2]' \ + --prop text="120 ▲" --prop bold=true --prop fill=C6E0B4 +``` + +`@name=` / `@id=` addressing is also accepted on `get`, `query`, `remove`. + +**Features:** `style=medium1..4|light1..3|dark1..2|none`, +`firstRow`, `lastRow`, `firstCol`, `lastCol`, `bandedRows`, `bandedCols`, +`rowHeight`, `name` + `/table[@name=…]` addressing. diff --git a/examples/ppt/tables/tables-styled.pptx b/examples/ppt/tables/tables-styled.pptx new file mode 100644 index 000000000..8d27292cb Binary files /dev/null and b/examples/ppt/tables/tables-styled.pptx differ diff --git a/examples/ppt/tables/tables-styled.sh b/examples/ppt/tables/tables-styled.sh new file mode 100644 index 000000000..e226e30e3 --- /dev/null +++ b/examples/ppt/tables/tables-styled.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# PowerPoint built-in table styles showcase. +# Demonstrates: style= (medium1..4, light1..3, dark1..2, none), +# firstRow/lastRow/firstCol/lastCol/bandedRows/bandedCols banding flags. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/tables-styled.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +DATA="Region,Q1,Q2,Q3,Q4;North,120,135,142,168;South,98,110,121,140;East,165,178,190,205;West,140,155,168,182" + +add_slide () { + local idx="$1" style="$2" title="$3" + officecli add "$PPTX" /presentation/slides --type slide + officecli add "$PPTX" "/slide[$idx]" --type shape \ + --prop text="$title" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + officecli add "$PPTX" "/slide[$idx]" --type table \ + --prop x=0.5in --prop y=1.2in --prop width=12in --prop height=3in \ + --prop style="$style" \ + --prop firstRow=true --prop bandedRows=true \ + --prop data="$DATA" +} + +# 9 built-in styles, one per slide. +add_slide 1 medium1 "style=medium1" +add_slide 2 medium2 "style=medium2" +add_slide 3 medium3 "style=medium3" +add_slide 4 medium4 "style=medium4" +add_slide 5 light1 "style=light1" +add_slide 6 light2 "style=light2" +add_slide 7 light3 "style=light3" +add_slide 8 dark1 "style=dark1" +add_slide 9 dark2 "style=dark2" + +# Slide 10: banding flag combinations on a single style. +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[10]' --type shape \ + --prop text="Banding Flags (style=medium2)" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +officecli add "$PPTX" '/slide[10]' --type shape \ + --prop text="firstRow + bandedRows" --prop size=14 \ + --prop x=0.5in --prop y=1in --prop width=6in --prop height=0.4in +officecli add "$PPTX" '/slide[10]' --type table \ + --prop x=0.5in --prop y=1.4in --prop width=6in --prop height=2.5in \ + --prop style=medium2 --prop firstRow=true --prop bandedRows=true \ + --prop data="$DATA" + +officecli add "$PPTX" '/slide[10]' --type shape \ + --prop text="firstCol + bandedCols" --prop size=14 \ + --prop x=7in --prop y=1in --prop width=6in --prop height=0.4in +officecli add "$PPTX" '/slide[10]' --type table \ + --prop x=7in --prop y=1.4in --prop width=6in --prop height=2.5in \ + --prop style=medium2 --prop firstCol=true --prop bandedCols=true \ + --prop data="$DATA" + +officecli add "$PPTX" '/slide[10]' --type shape \ + --prop text="firstRow + lastRow (total row)" --prop size=14 \ + --prop x=0.5in --prop y=4.3in --prop width=6in --prop height=0.4in +officecli add "$PPTX" '/slide[10]' --type table \ + --prop x=0.5in --prop y=4.7in --prop width=6in --prop height=2.5in \ + --prop style=medium2 --prop firstRow=true --prop lastRow=true \ + --prop data="$DATA;Total,523,578,621,695" + +officecli add "$PPTX" '/slide[10]' --type shape \ + --prop text="style=none (no theme)" --prop size=14 \ + --prop x=7in --prop y=4.3in --prop width=6in --prop height=0.4in +officecli add "$PPTX" '/slide[10]' --type table \ + --prop x=7in --prop y=4.7in --prop width=6in --prop height=2.5in \ + --prop style=none --prop border.all="1pt solid 808080" \ + --prop data="$DATA" + +# --- Slide 11: rowHeight (uniform) + name (stable @name addressing) --- +officecli add "$PPTX" /presentation/slides --type slide +officecli add "$PPTX" '/slide[11]' --type shape \ + --prop text="rowHeight + name= addressing" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in +officecli add "$PPTX" '/slide[11]' --type shape \ + --prop text="The table below was created with name=SalesData and rowHeight=1cm. After creation, it can be addressed as /slide[11]/table[@name=SalesData] instead of by positional index — handy when slides are reordered or tables added/removed." \ + --prop size=12 \ + --prop x=0.5in --prop y=0.95in --prop width=12in --prop height=0.8in + +officecli add "$PPTX" '/slide[11]' --type table \ + --prop x=0.5in --prop y=2in --prop width=12in \ + --prop rows=5 --prop cols=4 --prop rowHeight=1cm \ + --prop name=SalesData --prop style=medium2 --prop firstRow=true \ + --prop data="Region,Q1,Q2,Q3;North,120,135,142;South,98,110,121;East,165,178,190;West,140,155,168" + +# Demonstrate @name addressing — set a cell via the stable name path. +officecli set "$PPTX" '/slide[11]/table[@name=SalesData]/tr[2]/tc[2]' \ + --prop text="120 ▲" --prop bold=true --prop fill=C6E0B4 + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/textboxes/textboxes-advanced.md b/examples/ppt/textboxes/textboxes-advanced.md new file mode 100644 index 000000000..734192041 --- /dev/null +++ b/examples/ppt/textboxes/textboxes-advanced.md @@ -0,0 +1,167 @@ +# Advanced PPT Textbox Typography + +Three files work together: + +- **textboxes-advanced.sh** — Shell script that builds the deck. +- **textboxes-advanced.pptx** — The generated 5-slide deck. +- **textboxes-advanced.md** — This file. + +Fills in the **child-element** properties (paragraph + run) that +`textboxes-basic` didn't reach. The two scripts together exhaust the +text-body property surface for PPT. + +## Regenerate + +```bash +cd examples/ppt +bash textboxes/textboxes-advanced.sh +# → textboxes/textboxes-advanced.pptx +``` + +## Slides + +### Slide 1 — Per-paragraph overrides + +One textbox; each paragraph carries its own `align=` and `lineSpacing=`. +The shape's defaults (`align=left`, single-spaced) apply only to +paragraphs that don't override them. + +```bash +officecli add file.pptx '/slide[1]/shape[2]' --type paragraph \ + --prop text="..." --prop align=center + +officecli add file.pptx '/slide[1]/shape[2]' --type paragraph \ + --prop text="..." --prop align=justify --prop lineSpacing=2x + +officecli add file.pptx '/slide[1]/shape[2]' --type paragraph \ + --prop text="..." --prop lineSpacing=18pt +``` + +Use this pattern any time a single text box needs mixed paragraph +formatting (titles + body, quoted block + commentary, etc.). + +### Slide 2 — Paragraph indents + +Five variations on the same long text: + +| Spec | Result | +|---|---| +| (default) | flush left, no indent | +| `marginLeft=1in` | whole paragraph shifted 1in right | +| `indent=0.5in` | first-line indent (book-style paragraph) | +| `marginLeft=0.6in indent=-0.5in` | hanging indent (bibliography-style) | +| `marginRight=2in` | text narrowed from the right edge | + +`indent=` accepts negative values, which is how you build a hanging +indent — combine with a positive `marginLeft=` so the first line +"hangs" outside the body block. + +Aliases: `leftindent`, `leftIndent`, `indentleft`. Both `indent` and +`marginLeft`/`marginRight` route through `SpacingConverter` so they +accept any length form (`0.5in`, `1.27cm`, `36pt`, or bare twips). + +### Slide 3 — Per-paragraph styling (no runs needed) + +When a whole paragraph shares the same styling, set `bold` / `italic` / +`color` / `size` / `lang` directly on the paragraph instead of +wrapping the text in a run. It's cheaper and the OOXML is cleaner. + +```bash +officecli add file.pptx '/slide[3]/shape[2]' --type paragraph \ + --prop text="Whole paragraph is bold." --prop bold=true + +officecli add file.pptx '/slide[3]/shape[2]' --type paragraph \ + --prop text="Whole paragraph is red." --prop color=E63946 + +officecli add file.pptx '/slide[3]/shape[2]' --type paragraph \ + --prop text="Whole paragraph is 22pt." --prop size=22 + +officecli add file.pptx '/slide[3]/shape[2]' --type paragraph \ + --prop text="French paragraph." --prop lang=fr-FR +``` + +The paragraph emits one implicit run with the merged rPr properties. + +### Slide 4 — Per-run typography inside one paragraph + +When **part** of a paragraph needs different styling, switch to runs. +This slide demonstrates the four run-only props not reached by the +basic example: + +**`font=` per run** — mixed fonts in one line: + +```bash +officecli add file.pptx '/slide[4]/shape[1]/p[1]' --type run \ + --prop text="Times " --prop font="Times New Roman" --prop size=24 +officecli add file.pptx '/slide[4]/shape[1]/p[1]' --type run \ + --prop text="Courier " --prop font="Courier New" --prop size=18 +``` + +**`spacing=` per run** — tracking variation: + +```bash +--prop spacing=-1 # tighter +--prop spacing=4 # looser +--prop spacing=8 # very loose +``` + +Run-level `spacing=` is in **points** (decimal), shape-level `spacing=` +is in 1/100 pt (integer). Two different units for the same OOXML +attribute — handler emits whichever scope you asked for. + +**`kern=` per run** — kerning threshold: + +```bash +--prop kern=0 # disabled (no kerning at any size) +--prop kern=1 # enabled at all sizes (0.01pt threshold) +--prop kern=1200 # only kern from 12pt up +``` + +**`lang=` per run** — BCP-47 tag scoped to one run: + +```bash +--prop lang=en-US # color +--prop lang=en-GB # colour +--prop lang=fr-FR # couleur +``` + +PowerPoint's spellcheck honors per-run lang — useful for prose that +quotes terms from another language. + +### Slide 5 — `subscript` / `superscript` aliases vs `baseline=` + +Two equivalent ways to express the same OOXML attribute: + +```bash +# Convenience aliases — boolean +--prop subscript=true # ≡ baseline=sub +--prop superscript=true # ≡ baseline=super + +# Canonical — signed integer percent +--prop baseline=sub # ≡ -25 +--prop baseline=super # ≡ +30 +--prop baseline=50 # custom: 50% raise +--prop baseline=-40 # custom: 40% drop +``` + +Get readback always uses canonical `baseline` (signed integer +percent). `subscript` / `superscript` are mutually exclusive — set one +to clear the other. + +Slide 5 also covers per-run `cap` directly on Add: + +```bash +--prop cap=small # canonical +--prop cap=all +--prop cap=none +--prop allCaps=true # boolean alias → cap=all +--prop smallCaps=true # boolean alias → cap=small +--prop allCaps=false # → cap=none +``` + +**Features covered:** per-paragraph `align`/`lineSpacing` overrides +inside one shape, `indent` (positive & negative for hanging-indent), +`marginLeft`, `marginRight`, per-paragraph `bold`/`italic`/`color`/`size`/`lang`, +per-run `font`, per-run `spacing` (points), per-run `kern`, per-run `lang`, +per-run `cap` (with `allCaps` / `smallCaps` aliases), +`subscript`/`superscript` aliases, custom `baseline=` percent. diff --git a/examples/ppt/textboxes/textboxes-advanced.pptx b/examples/ppt/textboxes/textboxes-advanced.pptx new file mode 100644 index 000000000..88dcde8fd Binary files /dev/null and b/examples/ppt/textboxes/textboxes-advanced.pptx differ diff --git a/examples/ppt/textboxes/textboxes-advanced.sh b/examples/ppt/textboxes/textboxes-advanced.sh new file mode 100755 index 000000000..8864694af --- /dev/null +++ b/examples/ppt/textboxes/textboxes-advanced.sh @@ -0,0 +1,259 @@ +#!/bin/bash +# Advanced PPT textbox typography — per-paragraph and per-run overrides that the +# basic example didn't reach. Each slide demonstrates a different scope: +# slide 1 — per-paragraph align / lineSpacing override (shape default vs paragraph) +# slide 2 — paragraph indents (indent / marginLeft / marginRight) for hanging-indent style +# slide 3 — per-paragraph styling (bold / italic / color / size / lang) without runs +# slide 4 — per-run typography (font / size / spacing / kern / lang) inside one paragraph +# slide 5 — subscript / superscript convenience aliases vs canonical baseline= + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/textboxes-advanced.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +LOREM='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum vestibulum.' + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 1 — Per-paragraph overrides (align / lineSpacing inside one textbox) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop text="Per-paragraph overrides inside one textbox" \ + --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# One textbox; shape-level defaults are align=left, lineSpacing=1x. +# Each paragraph overrides one of them. +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop x=0.5in --prop y=1.2in --prop width=13in --prop height=5.5in \ + --prop fill=F1FAEE --prop size=14 \ + --prop text="[shape default: align=left, single-spaced] $LOREM" + +officecli add "$PPTX" '/slide[1]/shape[2]' --type paragraph \ + --prop text="[paragraph override: align=center] $LOREM" --prop align=center + +officecli add "$PPTX" '/slide[1]/shape[2]' --type paragraph \ + --prop text="[paragraph override: align=right] $LOREM" --prop align=right + +officecli add "$PPTX" '/slide[1]/shape[2]' --type paragraph \ + --prop text="[paragraph override: align=justify + lineSpacing=2x] $LOREM $LOREM" \ + --prop align=justify --prop lineSpacing=2x + +officecli add "$PPTX" '/slide[1]/shape[2]' --type paragraph \ + --prop text="[paragraph override: lineSpacing=18pt fixed] $LOREM" \ + --prop lineSpacing=18pt + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 2 — Paragraph indents (indent / marginLeft / marginRight) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop text="Paragraph indents — indent / marginLeft / marginRight" \ + --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Reference (no indent) +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=1.3in --prop width=13in --prop height=1in \ + --prop fill=F1FAEE --prop size=14 \ + --prop text="[default: no indent] $LOREM $LOREM" + +# Left indent (whole paragraph shifted right) +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=2.5in --prop width=13in --prop height=1in \ + --prop fill=A8DADC --prop size=14 \ + --prop text="[marginLeft=1in] $LOREM $LOREM" \ + --prop marginLeft=1in + +# First-line indent (only first line is shifted; rest flush left) +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=3.7in --prop width=13in --prop height=1in \ + --prop fill=F4A261 --prop size=14 \ + --prop text="[indent=0.5in first-line] $LOREM $LOREM" \ + --prop indent=0.5in + +# Hanging indent (negative indent + positive marginLeft pulls first line back left) +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=4.9in --prop width=13in --prop height=1in \ + --prop fill=A8DADC --prop size=14 \ + --prop text="[hanging: marginLeft=0.6in + indent=-0.5in] $LOREM $LOREM" \ + --prop marginLeft=0.6in --prop indent=-0.5in + +# Right margin (text narrowed from the right) +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=6.1in --prop width=13in --prop height=1in \ + --prop fill=F4A261 --prop size=14 \ + --prop text="[marginRight=2in] $LOREM $LOREM" \ + --prop marginRight=2in + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 3 — Per-paragraph styling (bold / italic / color / size / lang) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop text="Per-paragraph styling (no runs needed)" \ + --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# One textbox; each paragraph carries its own bold/italic/color/size/lang. +# This is cheaper than adding a run when the whole paragraph shares one style. +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop x=0.5in --prop y=1.2in --prop width=13in --prop height=5in \ + --prop fill=F1FAEE --prop size=14 \ + --prop text="[shape default: 14pt black] Default paragraph styling." + +officecli add "$PPTX" '/slide[3]/shape[2]' --type paragraph \ + --prop text="[bold=true at paragraph level] Whole paragraph is bold." \ + --prop bold=true + +officecli add "$PPTX" '/slide[3]/shape[2]' --type paragraph \ + --prop text="[italic=true at paragraph level] Whole paragraph is italic." \ + --prop italic=true + +officecli add "$PPTX" '/slide[3]/shape[2]' --type paragraph \ + --prop text="[color=E63946 at paragraph level] Whole paragraph is red." \ + --prop color=E63946 + +officecli add "$PPTX" '/slide[3]/shape[2]' --type paragraph \ + --prop text="[size=22 at paragraph level] Whole paragraph is 22pt." \ + --prop size=22 + +officecli add "$PPTX" '/slide[3]/shape[2]' --type paragraph \ + --prop text="[lang=fr-FR at paragraph level] Lorem ipsum dolor sit amet." \ + --prop lang=fr-FR --prop color=2A9D8F + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 4 — Per-run typography (font / size / spacing / kern / lang in one paragraph) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop text="Per-run typography in one paragraph" \ + --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Empty textbox we'll build run-by-run +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=0.5in --prop y=1.5in --prop width=13in --prop height=1in \ + --prop text="" --prop size=20 + +officecli add "$PPTX" '/slide[4]/shape[1]/p[1]' --type run --prop text="Mix " +officecli add "$PPTX" '/slide[4]/shape[1]/p[1]' --type run \ + --prop text="Times " --prop font="Times New Roman" --prop size=24 +officecli add "$PPTX" '/slide[4]/shape[1]/p[1]' --type run \ + --prop text="Courier " --prop font="Courier New" --prop size=18 +officecli add "$PPTX" '/slide[4]/shape[1]/p[1]' --type run \ + --prop text="Georgia" --prop font="Georgia" --prop size=28 --prop bold=true + +# Per-run character spacing +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=0.5in --prop y=3in --prop width=13in --prop height=1in \ + --prop text="" --prop size=20 --prop bold=true + +officecli add "$PPTX" '/slide[4]/shape[2]/p[1]' --type run --prop text="Normal " +officecli add "$PPTX" '/slide[4]/shape[2]/p[1]' --type run \ + --prop text="TIGHTENED " --prop spacing=-1 --prop color=E63946 +officecli add "$PPTX" '/slide[4]/shape[2]/p[1]' --type run \ + --prop text="LOOSENED " --prop spacing=4 --prop color=2A9D8F +officecli add "$PPTX" '/slide[4]/shape[2]/p[1]' --type run \ + --prop text="EXPANDED" --prop spacing=8 --prop color=1D3557 + +# Per-run kerning threshold +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=0.5in --prop y=4.3in --prop width=13in --prop height=1in \ + --prop text="" --prop size=20 --prop bold=true + +officecli add "$PPTX" '/slide[4]/shape[3]/p[1]' --type run \ + --prop text="AV AT WA — kern=0 " --prop kern=0 +officecli add "$PPTX" '/slide[4]/shape[3]/p[1]' --type run \ + --prop text="AV AT WA — kern=1" --prop kern=1 --prop color=E63946 + +# Per-run lang tag (drives spellcheck per-run) +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=0.5in --prop y=5.6in --prop width=13in --prop height=1in \ + --prop text="" --prop size=20 + +officecli add "$PPTX" '/slide[4]/shape[4]/p[1]' --type run \ + --prop text="English: color " --prop lang=en-US +officecli add "$PPTX" '/slide[4]/shape[4]/p[1]' --type run \ + --prop text="British: colour " --prop lang=en-GB --prop color=2A9D8F +officecli add "$PPTX" '/slide[4]/shape[4]/p[1]' --type run \ + --prop text="Français: couleur" --prop lang=fr-FR --prop color=E63946 + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 5 — subscript / superscript aliases vs canonical baseline= +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[5]' --type textbox \ + --prop text="subscript / superscript aliases" \ + --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Convenience form: subscript=true and superscript=true +officecli add "$PPTX" '/slide[5]' --type textbox \ + --prop x=0.5in --prop y=1.5in --prop width=13in --prop height=1in \ + --prop text="" --prop size=24 + +officecli add "$PPTX" '/slide[5]/shape[1]/p[1]' --type run --prop text="H" +officecli add "$PPTX" '/slide[5]/shape[1]/p[1]' --type run \ + --prop text="2" --prop subscript=true +officecli add "$PPTX" '/slide[5]/shape[1]/p[1]' --type run --prop text="SO" +officecli add "$PPTX" '/slide[5]/shape[1]/p[1]' --type run \ + --prop text="4" --prop subscript=true +officecli add "$PPTX" '/slide[5]/shape[1]/p[1]' --type run --prop text=" x" +officecli add "$PPTX" '/slide[5]/shape[1]/p[1]' --type run \ + --prop text="2" --prop superscript=true +officecli add "$PPTX" '/slide[5]/shape[1]/p[1]' --type run --prop text=" + y" +officecli add "$PPTX" '/slide[5]/shape[1]/p[1]' --type run \ + --prop text="2" --prop superscript=true +officecli add "$PPTX" '/slide[5]/shape[1]/p[1]' --type run --prop text=" = r" +officecli add "$PPTX" '/slide[5]/shape[1]/p[1]' --type run \ + --prop text="2" --prop superscript=true + +officecli add "$PPTX" '/slide[5]' --type textbox \ + --prop text='subscript=true ≡ baseline=sub superscript=true ≡ baseline=super' \ + --prop size=14 --prop italic=true --prop color=666666 \ + --prop x=0.5in --prop y=2.8in --prop width=13in --prop height=0.5in + +# Custom baseline percent — neither alias gives you this +officecli add "$PPTX" '/slide[5]' --type textbox \ + --prop x=0.5in --prop y=3.7in --prop width=13in --prop height=1in \ + --prop text="" --prop size=24 + +officecli add "$PPTX" '/slide[5]/shape[3]/p[1]' --type run --prop text="Custom: " +officecli add "$PPTX" '/slide[5]/shape[3]/p[1]' --type run \ + --prop text="50%" --prop baseline=50 --prop color=E63946 +officecli add "$PPTX" '/slide[5]/shape[3]/p[1]' --type run --prop text=" higher / " +officecli add "$PPTX" '/slide[5]/shape[3]/p[1]' --type run \ + --prop text="-40%" --prop baseline=-40 --prop color=2A9D8F +officecli add "$PPTX" '/slide[5]/shape[3]/p[1]' --type run --prop text=" lower" + +officecli add "$PPTX" '/slide[5]' --type textbox \ + --prop text='baseline= accepts signed integer percent (super≡+30, sub≡-25 by convention). Custom values give arbitrary vertical offset.' \ + --prop size=14 --prop italic=true --prop color=666666 \ + --prop x=0.5in --prop y=5in --prop width=13in --prop height=0.6in + +# Per-run case rendering — cap on run Add accepts cap / allCaps / smallCaps +# directly (canonical + aliases). Same enum surface as the shape-level Set. +officecli add "$PPTX" '/slide[5]' --type textbox \ + --prop x=0.5in --prop y=5.9in --prop width=13in --prop height=0.8in \ + --prop text="" --prop size=20 --prop bold=true + +officecli add "$PPTX" '/slide[5]/shape[4]/p[1]' --type run --prop text="default " +officecli add "$PPTX" '/slide[5]/shape[4]/p[1]' --type run \ + --prop text="small caps " --prop cap=small --prop color=2A9D8F +officecli add "$PPTX" '/slide[5]/shape[4]/p[1]' --type run \ + --prop text="ALL CAPS" --prop allCaps=true --prop color=E63946 + +officecli add "$PPTX" '/slide[5]' --type textbox \ + --prop text='Per-run cap=small / cap=all / cap=none, plus allCaps / smallCaps boolean aliases.' \ + --prop size=12 --prop italic=true --prop color=666666 \ + --prop x=0.5in --prop y=6.8in --prop width=13in --prop height=0.5in + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/textboxes/textboxes-basic.md b/examples/ppt/textboxes/textboxes-basic.md new file mode 100644 index 000000000..57d069aab --- /dev/null +++ b/examples/ppt/textboxes/textboxes-basic.md @@ -0,0 +1,140 @@ +# Basic PPT Textboxes + +Three files work together: + +- **textboxes-basic.sh** — Shell script that calls `officecli` to build the deck. +- **textboxes-basic.pptx** — The generated 4-slide deck. +- **textboxes-basic.md** — This file. + +## Regenerate + +```bash +cd examples/ppt +bash textboxes/textboxes-basic.sh +# → textboxes/textboxes-basic.pptx +``` + +> 📌 `textbox` is an **alias** for `shape` in PPT (no separate +> element). Both route to `AddShape` — the only difference is that +> `--type textbox` skips the default `geometry=rect` outline and goes +> straight to a text-body container. The full property surface +> documented under `pptx/shape.json` applies. + +## Slides + +### Slide 1 — Horizontal alignment + +Four full-width textboxes, one per `align=` value: + +| Value | Effect | +|---|---| +| `left` | flush-left (default) | +| `center` | centered | +| `right` | flush-right | +| `justify` | full-justified (last line ragged) | + +```bash +officecli add file.pptx /slide[1] --type textbox \ + --prop x=0.5in --prop y=1.3in --prop width=12in --prop height=1.3in \ + --prop text="..." --prop align=center +``` + +Aliases: `alignment`, `halign` both accepted on input. + +### Slide 2 — Lists and multi-paragraph + +Two patterns shown side-by-side: + +**Bullet list** — start with one paragraph at Add time, then append +the rest via `--type paragraph` on the shape, then turn on bullets in +one shot at the shape level: + +```bash +officecli add file.pptx /slide[2] --type textbox \ + --prop x=0.5in --prop y=1.2in --prop width=6in --prop height=4in \ + --prop text="Coffee preparation steps" + +officecli add file.pptx '/slide[2]/shape[1]' --type paragraph \ + --prop text="Grind beans to medium-fine" +officecli add file.pptx '/slide[2]/shape[1]' --type paragraph \ + --prop text="Heat water to 93°C" +# ... repeat for each line + +officecli set file.pptx '/slide[2]/shape[1]' --prop list=bullet +``` + +**Numbered list** — same pattern, `list=numbered` instead of `bullet`: + +| `list=` value | Output | +|---|---| +| `bullet` | • dots | +| `numbered` | 1. 2. 3. ... | +| `alpha` | a. b. c. ... | +| `roman` | i. ii. iii. ... | +| `none` | strip all list formatting | +| any single char | use that glyph as bullet | + +Nested levels via `--prop level=N` (0..8) on the paragraph — see the +last appended paragraph on slide 2's numbered list. + +### Slide 3 — Rich text via runs + +A textbox's `text=` is one big run. To mix styles inside one +paragraph, append `--type run` to the paragraph instead: + +```bash +# Empty textbox first (text="") +officecli add file.pptx /slide[3] --type textbox \ + --prop text="" --prop size=20 ... + +# Then build paragraph 1 run-by-run +officecli add file.pptx '/slide[3]/shape[1]/p[1]' --type run --prop text="The " +officecli add file.pptx '/slide[3]/shape[1]/p[1]' --type run \ + --prop text="quick " --prop bold=true --prop color=E63946 +officecli add file.pptx '/slide[3]/shape[1]/p[1]' --type run \ + --prop text="brown " --prop italic=true --prop color=A0522D +officecli add file.pptx '/slide[3]/shape[1]/p[1]' --type run --prop text="fox..." +``` + +Also demonstrated: + +- **Superscript / subscript**: `--prop baseline=super` / `--prop baseline=sub` + (also accepts signed integer percent: `baseline=-25`) +- **Strikethrough**: `--prop strike=single` (also `double`) +- **Per-run color / size / bold / italic / underline** — all standard + run-level rPr attributes + +> ⚠ `cap=all` and `cap=small` are accepted on **shape** Add/Set, but +> not on **run** Add (different code paths). For all-caps in a run, +> upper-case the source text instead. + +### Slide 4 — Multilingual + layout + +**Per-script fonts** — `font.latin`, `font.ea`, `font.cs` target +separate rPr font slots. PowerPoint uses one per script automatically: + +```bash +officecli add file.pptx /slide[4] --type textbox \ + --prop text="Hello, 世界! こんにちは、世界。" \ + --prop font.latin="Georgia" --prop font.ea="Yu Mincho" +``` + +ASCII characters use Georgia; Japanese/Chinese characters use Yu Mincho. +Aliases: `font.eastasia`, `font.eastasian`, `font.complexscript`, +`font.complex`. A bare `font=` sets both Latin and EA slots at once. + +For Arabic / Hebrew layouts add `--prop direction=rtl`. + +**Vertical alignment + padding** — three tall blue boxes show +`valign=top|middle|bottom`. The `margin=` prop sets uniform inner +padding (a.k.a. text-body inset): + +```bash +--prop valign=middle --prop margin=0.15in --prop align=center +``` + +**Features:** every `align=` value, bullet/numbered lists via shape-level +`list=`, paragraph `level=` for nested indents, run-by-run styling (bold, +italic, underline, color, strike, baseline=super/sub), per-script fonts +(`font.latin`/`font.ea`), `valign=top|middle|bottom`, `margin=` inner +padding. diff --git a/examples/ppt/textboxes/textboxes-basic.pptx b/examples/ppt/textboxes/textboxes-basic.pptx new file mode 100644 index 000000000..5015b941e Binary files /dev/null and b/examples/ppt/textboxes/textboxes-basic.pptx differ diff --git a/examples/ppt/textboxes/textboxes-basic.sh b/examples/ppt/textboxes/textboxes-basic.sh new file mode 100755 index 000000000..30e3604e6 --- /dev/null +++ b/examples/ppt/textboxes/textboxes-basic.sh @@ -0,0 +1,168 @@ +#!/bin/bash +# Basic PowerPoint textboxes — alignment, multi-paragraph, bulleted lists, +# styled runs, per-script fonts (Latin/EastAsian), vertical alignment, padding. +# Demonstrates: --type textbox vs --type shape (textbox is an alias for shape), +# --type paragraph with align/lineSpacing/indent, --type run with bold/color/baseline, +# shape-level list=bullet|ordered, font.latin/font.ea, valign and margin/autoFit. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/textboxes-basic.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 1 — Horizontal alignment (4 textboxes, one per align value) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop text="Horizontal Alignment" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +LOREM='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum vestibulum.' + +Y=1.3 +for a in left center right justify; do + officecli add "$PPTX" '/slide[1]' --type textbox \ + --prop x=0.5in --prop y="${Y}in" --prop width=12in --prop height=1.3in \ + --prop fill=F1FAEE --prop text="[align=$a] $LOREM" --prop size=14 \ + --prop align="$a" + Y=$(echo "$Y + 1.5" | bc -l) +done + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 2 — Multi-paragraph + bulleted / numbered lists +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop text="Lists and Multi-Paragraph" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Bulleted list — start with one initial paragraph, append the rest, then turn on bullets. +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=0.5in --prop y=1.2in --prop width=6in --prop height=4in \ + --prop text="Coffee preparation steps" \ + --prop bold=true --prop size=18 --prop color=1D3557 + +officecli add "$PPTX" '/slide[2]/shape[1]' --type paragraph --prop text="Grind beans to medium-fine" +officecli add "$PPTX" '/slide[2]/shape[1]' --type paragraph --prop text="Heat water to 93°C" +officecli add "$PPTX" '/slide[2]/shape[1]' --type paragraph --prop text="Bloom 30s with 2× coffee weight" +officecli add "$PPTX" '/slide[2]/shape[1]' --type paragraph --prop text="Pour remaining water in spirals" +officecli add "$PPTX" '/slide[2]/shape[1]' --type paragraph --prop text="Total brew time: 3-4 minutes" + +# Turn paragraphs 2-6 into bullets (level 0). Paragraph 1 is the title — leave unbulleted. +officecli set "$PPTX" '/slide[2]/shape[1]' --prop list=bullet + +# Numbered list (ordered) +officecli add "$PPTX" '/slide[2]' --type textbox \ + --prop x=7in --prop y=1.2in --prop width=6in --prop height=4in \ + --prop text="Release checklist" \ + --prop bold=true --prop size=18 --prop color=1D3557 + +officecli add "$PPTX" '/slide[2]/shape[2]' --type paragraph --prop text="Run tests" +officecli add "$PPTX" '/slide[2]/shape[2]' --type paragraph --prop text="Tag the release" +officecli add "$PPTX" '/slide[2]/shape[2]' --type paragraph --prop text="Push to registry" +officecli add "$PPTX" '/slide[2]/shape[2]' --type paragraph --prop text="Announce in #releases" + +officecli set "$PPTX" '/slide[2]/shape[2]' --prop list=numbered + +# Indented sub-bullet — level=1 on a paragraph nests it one step in. +officecli add "$PPTX" '/slide[2]/shape[2]' --type paragraph \ + --prop text="(verify checksum)" --prop level=1 +officecli set "$PPTX" '/slide[2]/shape[2]' --prop list=numbered + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 3 — Styled runs (rich text within one paragraph) +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop text="Rich Text — Runs" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Empty paragraph that we'll fill with multiple runs of different styles. +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop x=0.5in --prop y=1.5in --prop width=12in --prop height=1in \ + --prop text="" --prop size=20 + +officecli add "$PPTX" '/slide[3]/shape[1]/p[1]' --type run --prop text="The " +officecli add "$PPTX" '/slide[3]/shape[1]/p[1]' --type run \ + --prop text="quick " --prop bold=true --prop color=E63946 +officecli add "$PPTX" '/slide[3]/shape[1]/p[1]' --type run \ + --prop text="brown " --prop italic=true --prop color=A0522D +officecli add "$PPTX" '/slide[3]/shape[1]/p[1]' --type run --prop text="fox jumps over the " +officecli add "$PPTX" '/slide[3]/shape[1]/p[1]' --type run \ + --prop text="lazy " --prop underline=single --prop color=2A9D8F +officecli add "$PPTX" '/slide[3]/shape[1]/p[1]' --type run --prop text="dog." + +# Superscript / subscript via baseline +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop x=0.5in --prop y=3in --prop width=12in --prop height=0.8in \ + --prop text="" --prop size=24 + +officecli add "$PPTX" '/slide[3]/shape[2]/p[1]' --type run --prop text="E = mc" +officecli add "$PPTX" '/slide[3]/shape[2]/p[1]' --type run --prop text="2" --prop baseline=super +officecli add "$PPTX" '/slide[3]/shape[2]/p[1]' --type run --prop text=" and H" +officecli add "$PPTX" '/slide[3]/shape[2]/p[1]' --type run --prop text="2" --prop baseline=sub +officecli add "$PPTX" '/slide[3]/shape[2]/p[1]' --type run --prop text="O" + +# Strikethrough + small caps + colored +officecli add "$PPTX" '/slide[3]' --type textbox \ + --prop x=0.5in --prop y=4.2in --prop width=12in --prop height=0.8in \ + --prop text="" --prop size=20 + +officecli add "$PPTX" '/slide[3]/shape[3]/p[1]' --type run \ + --prop text="OLD PRICE: \$99 " --prop strike=single --prop color=999999 +officecli add "$PPTX" '/slide[3]/shape[3]/p[1]' --type run \ + --prop text="NOW \$49!" --prop bold=true --prop color=E63946 --prop size=24 + +# ───────────────────────────────────────────────────────────────────────────── +# Slide 4 — Per-script fonts (Latin + East Asian) + vertical alignment + padding +# ───────────────────────────────────────────────────────────────────────────── +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop text="Multilingual Fonts + Layout" --prop size=28 --prop bold=true \ + --prop x=0.5in --prop y=0.3in --prop width=12in --prop height=0.6in + +# Mixed-script box: separate fonts for Latin and EastAsian text. +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=0.5in --prop y=1.5in --prop width=6in --prop height=2in \ + --prop fill=F1FAEE --prop margin=0.2in \ + --prop text="Hello, 世界! こんにちは、世界。" \ + --prop size=24 --prop bold=true \ + --prop font.latin="Georgia" --prop font.ea="Yu Mincho" + +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=0.5in --prop y=3.7in --prop width=6in --prop height=0.5in \ + --prop text='font.latin=Georgia, font.ea="Yu Mincho"' \ + --prop size=12 --prop italic=true --prop color=666666 + +# Vertical alignment within a tall box +for va in top middle bottom; do + case $va in + top) X=7 ;; + middle) X=9.5 ;; + bottom) X=12 ;; # off-slide — squeeze + esac +done + +X=7 +for va in top middle bottom; do + officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x="${X}in" --prop y=1.5in --prop width=2in --prop height=3in \ + --prop fill=A8DADC --prop margin=0.15in \ + --prop text="valign=$va" --prop size=16 --prop bold=true \ + --prop valign="$va" --prop align=center + X=$(echo "$X + 2.2" | bc -l) +done + +officecli add "$PPTX" '/slide[4]' --type textbox \ + --prop x=7in --prop y=4.8in --prop width=6in --prop height=0.5in \ + --prop text='valign + per-box margin + align=center' \ + --prop size=12 --prop italic=true --prop color=666666 + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/transitions/transitions-bands.md b/examples/ppt/transitions/transitions-bands.md new file mode 100644 index 000000000..4b26dc0d3 --- /dev/null +++ b/examples/ppt/transitions/transitions-bands.md @@ -0,0 +1,65 @@ +# Band-Pattern PPT Transitions + +Three files work together: + +- **transitions-bands.sh** — Build script. +- **transitions-bands.pptx** — 21-slide deck. +- **transitions-bands.md** — This file. + +## Regenerate + +```bash +cd examples/ppt/transitions +bash transitions-bands.sh +# → transitions-bands.pptx +``` + +## Three pattern flavors + +### Orientation modifier (-horizontal / -vertical) + +```bash +officecli set deck.pptx /slide[N] --prop transition=blinds-vertical +officecli set deck.pptx /slide[N] --prop transition=checker-horizontal +officecli set deck.pptx /slide[N] --prop transition=comb-vertical +officecli set deck.pptx /slide[N] --prop transition=bars-horizontal +``` + +The default is `horizontal`; explicit writes round-trip as +`blinds-horizontal` even when matching the default (the readback path +preserves the explicit form, matching the wipe/push convention). + +### Corner direction (-leftup / -rightup / -leftdown / -rightdown) + +```bash +officecli set deck.pptx /slide[N] --prop transition=strips-leftup +officecli set deck.pptx /slide[N] --prop transition=strips-rightdown # default +``` + +### Orient × in/out (split needs BOTH) + +```bash +officecli set deck.pptx /slide[N] --prop transition=split-vertical-in +officecli set deck.pptx /slide[N] --prop transition=split-horizontal-out +``` + +`split-vertical` (orient without in/out) defaults `dir=in`, so it +round-trips canonically as `split-vertical-in`. Bare `split` (no +orientation given) reads back as plain `split`. + +## Aliases (input only, canonicalize on readback) + +| Input alias | Canonical readback | +|---|---| +| `venetian` | `blinds` | +| `checkerboard` | `checker` | +| `randombar` | `bars` | +| `diagonal` | `strips` | + +Pick whichever spelling reads best in your script; both produce the +same transition. + +## Related + +- [transitions-shapes.md](transitions-shapes.md) — circle/diamond/wheel (the non-banded geometric family). +- [transitions-directional.md](transitions-directional.md) — push/cover/wipe (cardinal-direction transitions). diff --git a/examples/ppt/transitions/transitions-bands.pptx b/examples/ppt/transitions/transitions-bands.pptx new file mode 100644 index 000000000..acb0dc770 Binary files /dev/null and b/examples/ppt/transitions/transitions-bands.pptx differ diff --git a/examples/ppt/transitions/transitions-bands.sh b/examples/ppt/transitions/transitions-bands.sh new file mode 100755 index 000000000..bf2a9bf0f --- /dev/null +++ b/examples/ppt/transitions/transitions-bands.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# Band / strip transitions — the slide reveals through parallel bands, +# checkerboard squares, or diagonal strips. +# +# Orientation modifier (-horizontal / -vertical): +# blinds, venetian (alias for blinds), checker, checkerboard (alias), +# comb, bars, randombar (alias for bars) +# +# Corner direction (-leftup / -rightup / -leftdown / -rightdown): +# strips, diagonal (alias for strips) +# +# Orient + in/out (BOTH must be specified for explicit form): +# split-vertical-in, split-horizontal-out, ... +# Bare 'split' rounds back as 'split'; 'split-vertical' alone now +# defaults dir=in (canonical readback: split-vertical-in). +# +# Aliases land on the canonical token at readback time: +# checkerboard → checker, randombar → bars, diagonal → strips, +# venetian → blinds. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/transitions-bands.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +n=0 +add_demo_slide() { + n=$((n+1)) + local trans=$1 title=$2 bg=$3 + officecli add "$PPTX" / --type slide + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill="$bg" + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop text="$title" --prop size=40 --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm + if [ -n "$trans" ]; then officecli set "$PPTX" "/slide[$n]" --prop transition="$trans"; fi +} + +add_demo_slide "" "Band Transitions" "1F3864" + +# Orientation: vertical vs horizontal +for combo in blinds-horizontal blinds-vertical \ + checker-horizontal checker-vertical \ + comb-horizontal comb-vertical \ + bars-horizontal bars-vertical; do + add_demo_slide "$combo" "$combo" "2E5C8A" +done + +# Strips: 4 corner directions +for d in leftup rightup leftdown rightdown; do + add_demo_slide "strips-$d" "strips-$d" "4F7C3A" +done + +# Split: orient × in/out matrix +for orient in horizontal vertical; do + for io in in out; do + add_demo_slide "split-$orient-$io" "split-$orient-$io" "8A5A2B" + done +done + +# Alias demo — same XML, different input spelling +add_demo_slide "venetian-vertical" "venetian-vertical (alias → blinds)" "7030A0" +add_demo_slide "checkerboard-vertical" "checkerboard-vertical (alias → checker)" "7030A0" +add_demo_slide "randombar-vertical" "randombar-vertical (alias → bars)" "7030A0" +add_demo_slide "diagonal-leftdown" "diagonal-leftdown (alias → strips)" "7030A0" + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/transitions/transitions-basic.md b/examples/ppt/transitions/transitions-basic.md new file mode 100644 index 000000000..f09bd2969 --- /dev/null +++ b/examples/ppt/transitions/transitions-basic.md @@ -0,0 +1,53 @@ +# Basic PPT Transitions + +Three files work together: + +- **transitions-basic.sh** — Shell script that calls `officecli` to build the deck. +- **transitions-basic.pptx** — The generated 6-slide deck. +- **transitions-basic.md** — This file. + +## Regenerate + +```bash +cd examples/ppt/transitions +bash transitions-basic.sh +# → transitions-basic.pptx +``` + +## What this trio shows + +The five "everyday" transition tokens, plus the canonical clear form. +Transitions on slide N animate the entry of slide N (replacing slide N-1). +Step through the deck in PowerPoint's Slide Show mode to see the actual +motion — the static .pptx looks identical for all of them. + +| Slide | Transition | Effect in playback | +|---|---|---| +| 1 | (none — entry slide) | No transition; baseline | +| 2 | `cut` | Instant swap, no animation | +| 3 | `fade` | Pixel cross-fade | +| 4 | `dissolve` | Speckle-pattern blend | +| 5 | `flash` | Quick white flash through | +| 6 | `none` cleared | Was `fade`, then `transition=none` removed it | + +## How `transition=none` clears + +```bash +officecli set deck.pptx /slide[6] --prop transition=fade +officecli set deck.pptx /slide[6] --prop transition=none +``` + +After the second call, `Get` returns no `transition` key — the +transition is fully cleared, even for Morph and other wrapped types. +Use this to remove a stale transition rather than guessing at an empty +string. + +## Related trios + +- [transitions-directional.md](transitions-directional.md) — push / cover / wipe with direction +- [transitions-shapes.md](transitions-shapes.md) — circle / diamond / wheel / zoom +- [transitions-bands.md](transitions-bands.md) — blinds / strips / split / checker +- [transitions-dynamic.md](transitions-dynamic.md) — Office 2010+ "Exciting" gallery +- [transitions-random.md](transitions-random.md) — newsflash / random +- [transitions-timing.md](transitions-timing.md) — speed, duration, advance +- [transitions-morph.md](transitions-morph.md) — Morph (2016+) diff --git a/examples/ppt/transitions/transitions-basic.pptx b/examples/ppt/transitions/transitions-basic.pptx new file mode 100644 index 000000000..ff82e0cda Binary files /dev/null and b/examples/ppt/transitions/transitions-basic.pptx differ diff --git a/examples/ppt/transitions/transitions-basic.sh b/examples/ppt/transitions/transitions-basic.sh new file mode 100755 index 000000000..4eaddbf93 --- /dev/null +++ b/examples/ppt/transitions/transitions-basic.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Basic slide transitions — cut, fade, dissolve, flash, and the 'none' clear. +# These five tokens form the everyday transition vocabulary: cut = instant, +# fade = pixel cross-fade, dissolve = pixel-noise dissolve, flash = white +# flash-through, none = remove an existing transition. +# +# Each demo slide carries the transition that triggers AS THE SLIDE ENTERS +# (replacing the previous one). To experience them, open the .pptx and step +# through Slide Show mode — most differences only show in playback. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/transitions-basic.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +# add_demo_slide N transition title-text bg-hex +add_demo_slide() { + local n=$1 trans=$2 title=$3 bg=$4 + officecli add "$PPTX" / --type slide + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm \ + --prop fill="$bg" + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop text="$title" --prop size=54 --prop bold=true --prop color=FFFFFF \ + --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm + if [ -n "$trans" ]; then + officecli set "$PPTX" "/slide[$n]" --prop transition="$trans" + fi +} + +add_demo_slide 1 "" "Basic Transitions" "1F3864" +add_demo_slide 2 "cut" "cut — instant swap" "C00000" +add_demo_slide 3 "fade" "fade — pixel cross-fade" "2E75B6" +add_demo_slide 4 "dissolve" "dissolve — speckle blend" "7030A0" +add_demo_slide 5 "flash" "flash — white flash-thru" "BF8F00" + +# Demonstrate the 'none' clear: slide 6 first gets fade, then we wipe it. +# Final readback on slide 6 should NOT have a transition key at all. +add_demo_slide 6 "fade" "none — fade cleared" "404040" +officecli set "$PPTX" "/slide[6]" --prop transition=none + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/transitions/transitions-directional.md b/examples/ppt/transitions/transitions-directional.md new file mode 100644 index 000000000..f755d2ed4 --- /dev/null +++ b/examples/ppt/transitions/transitions-directional.md @@ -0,0 +1,51 @@ +# Directional PPT Transitions + +Three files work together: + +- **transitions-directional.sh** — Build script. +- **transitions-directional.pptx** — 25-slide deck. +- **transitions-directional.md** — This file. + +## Regenerate + +```bash +cd examples/ppt/transitions +bash transitions-directional.sh +# → transitions-directional.pptx +``` + +## Family / direction matrix + +Direction support is **not uniform**. Mismatching the family triggers +an error (`Invalid slide direction: 'leftup'. Valid values: left, right, up, down.`): + +| Family | Direction set | +|---|---| +| `push` | `up` / `down` / `left` / `right` (4 cardinal) | +| `wipe` | `up` / `down` / `left` / `right` (4 cardinal) | +| `cover` | 4 cardinal **plus** `leftup` / `rightup` / `leftdown` / `rightdown` (8 total) | +| `uncover` | 8 directions, same set as `cover` | +| `pull` | Alias for `uncover` — same XML, same canonical readback | + +## Combined-token shorthand + +The combined input syntax is `TYPE[-DIR][-SPEED|DUR]`: + +```bash +officecli set deck.pptx /slide[2] --prop transition=push-right +officecli set deck.pptx /slide[2] --prop transition=cover-leftdown +officecli set deck.pptx /slide[2] --prop transition=wipe-up-slow +officecli set deck.pptx /slide[2] --prop transition=push-right-1500 +``` + +## Canonical readback + +`Get` returns the canonical full-word form: `push-right`, not `push-r`. +Single-letter abbreviations (`l`/`r`/`u`/`d`) are accepted on input but +always expand on readback so `set transition=pan-u` and +`set transition=pan-up` round-trip identically. + +## Aliases (input only) + +- `pull` accepts but reads back as `uncover` +- Single-letter dir abbreviations (`l`/`r`/`u`/`d`) accepted on input diff --git a/examples/ppt/transitions/transitions-directional.pptx b/examples/ppt/transitions/transitions-directional.pptx new file mode 100644 index 000000000..a7fc1854a Binary files /dev/null and b/examples/ppt/transitions/transitions-directional.pptx differ diff --git a/examples/ppt/transitions/transitions-directional.sh b/examples/ppt/transitions/transitions-directional.sh new file mode 100755 index 000000000..70f3fb26f --- /dev/null +++ b/examples/ppt/transitions/transitions-directional.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Directional transitions — push, cover, uncover/pull, wipe. +# +# Direction support is NOT uniform across the family: +# - push: up/down/left/right (4 dirs) +# - wipe: up/down/left/right (4 dirs) +# - cover/uncover: up/down/left/right + leftup/rightup/ +# leftdown/rightdown (8 dirs) +# - pull: alias for uncover (same 8 dirs) +# +# Mismatching the family/direction triggers an OOXML schema-level error +# from officecli (e.g. 'push-leftup' is rejected — push only supports the +# four cardinal directions). See transitions-basic.sh for the no-direction +# transitions (cut/fade/dissolve/flash). + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/transitions-directional.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +n=0 +add_demo_slide() { + n=$((n+1)) + local trans=$1 title=$2 bg=$3 + officecli add "$PPTX" / --type slide + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill="$bg" + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop text="$title" --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm + if [ -n "$trans" ]; then officecli set "$PPTX" "/slide[$n]" --prop transition="$trans"; fi +} + +add_demo_slide "" "Directional Transitions" "1F3864" + +# push: 4 cardinal directions +for d in up down left right; do + add_demo_slide "push-$d" "push-$d" "2E5C8A" +done + +# wipe: 4 cardinal directions +for d in up down left right; do + add_demo_slide "wipe-$d" "wipe-$d" "4F7C3A" +done + +# cover: 8 directions (4 cardinal + 4 diagonal corner) +for d in up down left right leftup rightup leftdown rightdown; do + add_demo_slide "cover-$d" "cover-$d" "8A5A2B" +done + +# uncover (a.k.a. pull): 8 directions +for d in up down left right leftup rightup leftdown rightdown; do + add_demo_slide "uncover-$d" "uncover-$d" "7030A0" +done + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/transitions/transitions-dynamic.md b/examples/ppt/transitions/transitions-dynamic.md new file mode 100644 index 000000000..d7ec86522 --- /dev/null +++ b/examples/ppt/transitions/transitions-dynamic.md @@ -0,0 +1,53 @@ +# Dynamic / 3D PPT Transitions (Office 2010+ "Exciting") + +Three files work together: + +- **transitions-dynamic.sh** — Build script. +- **transitions-dynamic.pptx** — 24-slide deck. +- **transitions-dynamic.md** — This file. + +## Regenerate + +```bash +cd examples/ppt/transitions +bash transitions-dynamic.sh +# → transitions-dynamic.pptx +``` + +## Why these are special + +These transitions ship in PowerPoint 2010 or later. officecli writes +each one with an inline fade fallback baked in, so a pre-2010 +PowerPoint opening the same deck plays a plain fade in their place +instead of failing or showing nothing. + +## Direction grouping + +| Family | Direction set | Example | +|---|---|---| +| LeftRight | `left` / `right` | `switch-right`, `flip-right`, `ferris-right`, `gallery-right`, `conveyor-right`, `reveal-right` | +| InOut | `in` / `out` | `shred-out`, `flythrough-out`, `warp-out` | +| SlideDir (4 cardinal) | `up` / `down` / `left` / `right` | `vortex-up`, `glitter-right`, `pan-up`, `prism-right` | +| Orientation | `horizontal` / `vertical` | `doors-vertical`, `window-horizontal` | +| (direction-less) | — | `ripple`, `honeycomb` | + +## Combined-token shorthand + +```bash +officecli set deck.pptx /slide[N] --prop transition=switch-right +officecli set deck.pptx /slide[N] --prop transition=shred-out-slow +officecli set deck.pptx /slide[N] --prop transition=ferris-right-1500 +``` + +## Recent fixes pinned by this trio + +- `reveal-right`, `ferris-right`, `gallery-right`, `conveyor-right`, + `shred-out`, `flythrough-out`, `warp-out` — direction was silently + dropped at write time and lost on readback; each now round-trips. +- `pan-up` previously read back as the truncated `pan-u`; single-letter + abbreviations now always expand to full words on readback. + +## Related + +- [transitions-basic.md](transitions-basic.md) — Office 97-era cut/fade/dissolve. +- [transitions-morph.md](transitions-morph.md) — Office 2016+ Morph (separate code path). diff --git a/examples/ppt/transitions/transitions-dynamic.pptx b/examples/ppt/transitions/transitions-dynamic.pptx new file mode 100644 index 000000000..545186f96 Binary files /dev/null and b/examples/ppt/transitions/transitions-dynamic.pptx differ diff --git a/examples/ppt/transitions/transitions-dynamic.sh b/examples/ppt/transitions/transitions-dynamic.sh new file mode 100755 index 000000000..8e6f36a12 --- /dev/null +++ b/examples/ppt/transitions/transitions-dynamic.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# 3D / dynamic transitions — PowerPoint 2010+ "Exciting" gallery. Each +# requires Office 2010+ to render; older PowerPoint silently falls back +# to fade (officecli emits an mc:AlternateContent wrapper with that +# fallback baked in). +# +# Direction families: +# left/right (LeftRightDir): switch, flip, ferris, gallery, conveyor, reveal +# in/out (InOutDir): shred, flythrough, warp +# up/down/left/right (SlideDir): vortex, glitter, pan, prism +# horizontal/vertical: doors, window +# no direction: ripple, honeycomb + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/transitions-dynamic.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +n=0 +add_demo_slide() { + n=$((n+1)) + local trans=$1 title=$2 bg=$3 + officecli add "$PPTX" / --type slide + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill="$bg" + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop text="$title" --prop size=40 --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm + if [ -n "$trans" ]; then officecli set "$PPTX" "/slide[$n]" --prop transition="$trans"; fi +} + +add_demo_slide "" "Dynamic Transitions" "1F3864" + +# LeftRight family +for t in switch flip ferris gallery conveyor reveal; do + add_demo_slide "$t-right" "$t-right" "2E5C8A" +done + +# InOut family +for t in shred flythrough warp; do + add_demo_slide "$t-out" "$t-out" "4F7C3A" +done + +# SlideDir family — 4 cardinal (prism is direction-less; see PrismFamily below) +for t in vortex glitter pan; do + for d in up right; do + add_demo_slide "$t-$d" "$t-$d" "8A5A2B" + done +done + +# Prism family — same element, 3 UI tiles via isContent/isInverted: +# prism / cube (alias) → "Cube" (Exciting) +# rotate (isContent=1) → "Rotate" (Dynamic Content) +# orbit (isContent=1 isInverted=1) → "Orbit" (Dynamic Content) +add_demo_slide "prism" "prism (== Cube in UI)" "6E3B23" +add_demo_slide "rotate" "rotate" "6E3B23" +add_demo_slide "orbit" "orbit" "6E3B23" + +# Horizontal/vertical orientation +for t in doors window; do + for d in horizontal vertical; do + add_demo_slide "$t-$d" "$t-$d" "7030A0" + done +done + +# Direction-less +for t in ripple honeycomb; do + add_demo_slide "$t" "$t" "C00000" +done + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/transitions/transitions-modern.md b/examples/ppt/transitions/transitions-modern.md new file mode 100644 index 000000000..060ad9e36 --- /dev/null +++ b/examples/ppt/transitions/transitions-modern.md @@ -0,0 +1,79 @@ +# Modern PPT Transitions (PowerPoint 2013+ "Exciting" Gallery) + +Three files work together: + +- **transitions-modern.sh** — Build script. +- **transitions-modern.pptx** — 19-slide deck. +- **transitions-modern.md** — This file. + +## Regenerate + +```bash +cd examples/ppt/transitions +bash transitions-modern.sh +# → transitions-modern.pptx +``` + +## What this trio shows + +The 12 "Exciting" / "Dynamic Content" presets PowerPoint 2013 added to +the Transitions gallery. Each one has an inline fade fallback baked +in, so pre-2013 PowerPoint plays a graceful fade in their place +instead of nothing. + +## The 12 presets + +| CLI token | UI name | Notes | +|---|---|---| +| `fallOver` | Fall Over | direction-sensitive | +| `drape` | Drape | direction-sensitive | +| `curtains` | Curtains | symmetric | +| `wind` | Wind | direction-sensitive | +| `prestige` | Prestige | symmetric | +| `fracture` | Fracture | symmetric | +| `crush` | Crush | symmetric | +| `peelOff` | Peel Off | direction-sensitive | +| `pageCurlDouble` | Page Curl (double) | direction-sensitive | +| `pageCurlSingle` | Page Curl (single) | direction-sensitive | +| `airplane` | Airplane | direction-sensitive | +| `origami` | Origami | direction-sensitive | + +Token spelling is lowerCamelCase. Input is case-insensitive +(`transition=PageCurlDouble` and `pagecurldouble` both work), but `Get` +returns the canonical lowerCamelCase form. + +## Direction (-in / -out) + +```bash +officecli set deck.pptx /slide[N] --prop transition=pageCurlDouble +officecli set deck.pptx /slide[N] --prop transition=pageCurlDouble-out +officecli set deck.pptx /slide[N] --prop transition=wind-out +``` + +- Default (no suffix) = `-in`. +- `-out` flips the Left/Right direction. Visually affects the + direction-sensitive presets in the table above; symmetric presets + (curtains, fracture, crush, prestige) parse the suffix but render + unchanged. +- Any other direction is rejected: + ``` + Error: Transition 'fallOver' only accepts -in or -out (got '-up'). + ``` + +## UI tiles backed by other tokens + +A few PowerPoint UI tiles that look like they belong in this gallery +are actually wired through other CLI tokens — just write the right one: + +| PowerPoint UI tile | CLI token | +|---|---| +| Cube (Exciting) | `prism` or `cube` | +| Rotate (Dynamic Content) | `rotate` | +| Orbit (Dynamic Content) | `orbit` | +| Clock (Exciting) | `wheel-1` or `clock` | + +## See also + +- [transitions-shapes.md](transitions-shapes.md) — Box lives there alongside circle/diamond/zoom. +- [transitions-dynamic.md](transitions-dynamic.md) — the older 2010 "Exciting" gallery (vortex / switch / flip / ferris / ... / prism / rotate / orbit). +- [transitions-morph.md](transitions-morph.md) — Morph (2016+). diff --git a/examples/ppt/transitions/transitions-modern.pptx b/examples/ppt/transitions/transitions-modern.pptx new file mode 100644 index 000000000..b86542856 Binary files /dev/null and b/examples/ppt/transitions/transitions-modern.pptx differ diff --git a/examples/ppt/transitions/transitions-modern.sh b/examples/ppt/transitions/transitions-modern.sh new file mode 100755 index 000000000..cb87bd5fb --- /dev/null +++ b/examples/ppt/transitions/transitions-modern.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# PowerPoint 2013+ "Exciting" / "Dynamic Content" transitions — +# the 12 presets stored as inside an +# mc:AlternateContent wrapper (PowerPoint <2013 plays the inline fade +# fallback). Token spelling matches the OOXML prst attribute +# (lowerCamelCase): fallOver, peelOff, pageCurlDouble, pageCurlSingle, +# etc. Box (covered in transitions-shapes) uses the same element. +# +# Direction modifier (-in / -out): +# default is -in (no invX/invY attributes written) +# -out sets invX="1" invY="1" — visually flips the transition axis on +# presets with a directional component (wind, peelOff, pageCurl*, +# airplane, origami, fallOver, drape). Symmetric presets (curtains, +# fracture, crush, prestige) accept the suffix but render unchanged. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/transitions-modern.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +n=0 +add_demo_slide() { + n=$((n+1)) + local trans=$1 title=$2 bg=$3 + officecli add "$PPTX" / --type slide + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill="$bg" + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop text="$title" --prop size=40 --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm + if [ -n "$trans" ]; then officecli set "$PPTX" "/slide[$n]" --prop transition="$trans"; fi +} + +add_demo_slide "" "Modern (p15) Transitions" "1F3864" + +# Each preset's bare form (= -in) +for t in fallOver drape curtains wind prestige fracture crush peelOff \ + pageCurlDouble pageCurlSingle airplane origami; do + add_demo_slide "$t" "$t" "2E5C8A" +done + +# A handful of -out variants showing the invX/invY flip on direction-sensitive presets +for t in wind peelOff pageCurlDouble airplane origami fallOver; do + add_demo_slide "$t-out" "$t-out" "8A5A2B" +done + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/transitions/transitions-morph.md b/examples/ppt/transitions/transitions-morph.md new file mode 100644 index 000000000..770a0c385 --- /dev/null +++ b/examples/ppt/transitions/transitions-morph.md @@ -0,0 +1,73 @@ +# Morph PPT Transition (Office 2016+) + +Three files work together: + +- **transitions-morph.sh** — Build script. +- **transitions-morph.pptx** — 4-slide deck. +- **transitions-morph.md** — This file. + +## Regenerate + +```bash +cd examples/ppt/transitions +bash transitions-morph.sh +# → transitions-morph.pptx +``` + +## What Morph does + +Unlike every other transition (which animates the *replacement* of one +slide by the next), Morph **tweens** content between adjacent slides +that share named objects. A shape with the same `name=` on slide N and +slide N+1, but with different x/y/width/height/rotation/fill, smoothly +interpolates from the slide-N position to the slide-N+1 position when +the transition plays. + +## Three granularities + +```bash +officecli set deck.pptx /slide[N] --prop transition=morph # default: byobject +officecli set deck.pptx /slide[N] --prop transition=morph-byword +officecli set deck.pptx /slide[N] --prop transition=morph-bychar +``` + +| Option | Tweens at the level of… | +|---|---| +| `byobject` (default) | Whole shape pairs (matched by name/id) | +| `byword` | Whole words within text bodies | +| `bychar` | Individual characters within text bodies | + +`object`, `word`, `char`, `character` are accepted input aliases; +`Get` returns the canonical `byObject` / `byWord` / `byChar` form. + +## How shape pairing works + +In this trio the script creates a shape `name=morphBall` on every +slide. Same name → PowerPoint pairs them across slides and animates +the geometry change as continuous motion. Without matching names, +shapes are treated as independent and fade in/out instead of tweening. + +```bash +# Slide 1 — small yellow ball, bottom-left +officecli add deck.pptx /slide[1] --type shape \ + --prop shape=ellipse --prop name=morphBall \ + --prop x=2cm --prop y=14cm --prop width=3cm --prop height=3cm + +# Slide 2 — same name, larger and centered → ball grows and moves +officecli set deck.pptx /slide[2] --prop transition=morph +officecli add deck.pptx /slide[2] --type shape \ + --prop shape=ellipse --prop name=morphBall \ + --prop x=15cm --prop y=10cm --prop width=6cm --prop height=6cm +``` + +## Backwards compatibility + +officecli writes morph with an inline fade fallback baked in. Older +PowerPoint (pre-2016) plays the fallback fade — the deck remains +openable everywhere. + +## See also + +- `examples/product_launch_morph.pptx` in the repo root — a full + product-launch deck built with morph as the primary motion. +- [transitions-dynamic.md](transitions-dynamic.md) — Office 2010+ "Exciting" gallery (vortex / switch / flip / ...). diff --git a/examples/ppt/transitions/transitions-morph.pptx b/examples/ppt/transitions/transitions-morph.pptx new file mode 100644 index 000000000..7c0823949 Binary files /dev/null and b/examples/ppt/transitions/transitions-morph.pptx differ diff --git a/examples/ppt/transitions/transitions-morph.sh b/examples/ppt/transitions/transitions-morph.sh new file mode 100755 index 000000000..55f413fa7 --- /dev/null +++ b/examples/ppt/transitions/transitions-morph.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# Morph transition — PowerPoint 2016+ smooth tweening between two slides +# that share named objects. Same shape on adjacent slides with different +# x/y/width/height/rotation is interpolated as continuous motion. +# +# Morph option (-byobject / -byword / -bychar): +# byobject (default) — shapes with matching IDs are paired and tweened +# byword — text body is morphed word-by-word +# bychar — text body is morphed character-by-character +# +# This trio is a starter demo. For a fuller scene-level showcase using +# this skill: see examples/product_launch_morph.pptx in the repo root. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/transitions-morph.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +# Slide 1: starting state (no transition — this is the entry point). +officecli add "$PPTX" / --type slide +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill=1F3864 +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop text="Morph" --prop size=72 --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm +# A named circle that will tween across slides 2/3/4. +officecli add "$PPTX" '/slide[1]' --type shape \ + --prop shape=ellipse --prop fill=FFC000 --prop name=morphBall \ + --prop x=2cm --prop y=14cm --prop width=3cm --prop height=3cm + +# Slide 2: morph-byobject — same-named ball moves right and grows. +officecli add "$PPTX" / --type slide +officecli set "$PPTX" '/slide[2]' --prop transition=morph +officecli add "$PPTX" '/slide[2]' --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill=2E5C8A +officecli add "$PPTX" '/slide[2]' --type shape \ + --prop text="morph (byobject — default)" --prop size=44 --prop bold=true \ + --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=2cm --prop width=29.87cm --prop height=3cm +officecli add "$PPTX" '/slide[2]' --type shape \ + --prop shape=ellipse --prop fill=FFC000 --prop name=morphBall \ + --prop x=15cm --prop y=10cm --prop width=6cm --prop height=6cm + +# Slide 3: morph-byword — title text recomposes word-by-word. +officecli add "$PPTX" / --type slide +officecli set "$PPTX" '/slide[3]' --prop transition=morph-byword +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill=4F7C3A +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop text="morph byword tweens words" --prop size=44 --prop bold=true \ + --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=2cm --prop width=29.87cm --prop height=3cm +officecli add "$PPTX" '/slide[3]' --type shape \ + --prop shape=ellipse --prop fill=FFC000 --prop name=morphBall \ + --prop x=27cm --prop y=14cm --prop width=3cm --prop height=3cm + +# Slide 4: morph-bychar — recomposes letter-by-letter. +officecli add "$PPTX" / --type slide +officecli set "$PPTX" '/slide[4]' --prop transition=morph-bychar +officecli add "$PPTX" '/slide[4]' --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill=8A5A2B +officecli add "$PPTX" '/slide[4]' --type shape \ + --prop text="bychar tweens letters" --prop size=44 --prop bold=true \ + --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=2cm --prop width=29.87cm --prop height=3cm +officecli add "$PPTX" '/slide[4]' --type shape \ + --prop shape=ellipse --prop fill=FFC000 --prop name=morphBall \ + --prop x=14cm --prop y=14cm --prop width=4cm --prop height=4cm + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/transitions/transitions-random.md b/examples/ppt/transitions/transitions-random.md new file mode 100644 index 000000000..0950a829d --- /dev/null +++ b/examples/ppt/transitions/transitions-random.md @@ -0,0 +1,38 @@ +# Random PPT Transitions + +Three files work together: + +- **transitions-random.sh** — Build script. +- **transitions-random.pptx** — 4-slide deck. +- **transitions-random.md** — This file. + +## Regenerate + +```bash +cd examples/ppt/transitions +bash transitions-random.sh +# → transitions-random.pptx +``` + +## What this trio shows + +```bash +officecli set deck.pptx /slide[N] --prop transition=newsflash +officecli set deck.pptx /slide[N] --prop transition=random +``` + +- **`newsflash`** — a fixed legacy animation: the new slide spins + inward newspaper-style. Pre-2010 element, no compatibility wrapper + needed. +- **`random`** — at render time, PowerPoint picks a random transition + from its available set. The .pptx only captures the *intent*; the + motion you see in Slide Show mode will differ each time you enter + presentation mode, even for the same slide. + +To experience the difference: open `transitions-random.pptx` in +PowerPoint, run Slide Show, exit, run Slide Show again — slides 3 and +4 should animate differently each pass. + +## Related + +- [transitions-basic.md](transitions-basic.md) — for the deterministic counterparts (cut/fade/dissolve/flash). diff --git a/examples/ppt/transitions/transitions-random.pptx b/examples/ppt/transitions/transitions-random.pptx new file mode 100644 index 000000000..0aeeec242 Binary files /dev/null and b/examples/ppt/transitions/transitions-random.pptx differ diff --git a/examples/ppt/transitions/transitions-random.sh b/examples/ppt/transitions/transitions-random.sh new file mode 100755 index 000000000..d8e236199 --- /dev/null +++ b/examples/ppt/transitions/transitions-random.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Random transitions — PowerPoint picks the actual animation at render +# time, so the .pptx only captures the intent, not the specific motion. +# +# newsflash — newspaper-style spin-and-zoom (one fixed animation, but +# grouped with the random family because it's pre-2010 +# legacy and rarely used outside this slot). +# random — PowerPoint chooses a random transition each time you +# enter Slide Show mode. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/transitions-random.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +n=0 +add_demo_slide() { + n=$((n+1)) + local trans=$1 title=$2 bg=$3 + officecli add "$PPTX" / --type slide + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill="$bg" + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop text="$title" --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm + if [ -n "$trans" ]; then officecli set "$PPTX" "/slide[$n]" --prop transition="$trans"; fi +} + +add_demo_slide "" "Random Transitions" "1F3864" +add_demo_slide "newsflash" "newsflash" "C00000" +# Run Slide Show twice on this deck — slide 3 should animate differently each time. +add_demo_slide "random" "random (re-rolls each play)" "2E75B6" +add_demo_slide "random" "random (different again)" "7030A0" + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/transitions/transitions-shapes.md b/examples/ppt/transitions/transitions-shapes.md new file mode 100644 index 000000000..08bea44f5 --- /dev/null +++ b/examples/ppt/transitions/transitions-shapes.md @@ -0,0 +1,73 @@ +# Shape-Mask PPT Transitions + +Three files work together: + +- **transitions-shapes.sh** — Build script. +- **transitions-shapes.pptx** — 12-slide deck. +- **transitions-shapes.md** — This file. + +## Regenerate + +```bash +cd examples/ppt/transitions +bash transitions-shapes.sh +# → transitions-shapes.pptx +``` + +## The three sub-families + +Shape-mask transitions reveal the new slide through a growing geometric +mask. They split into three flavors by what modifies the shape: + +### 1. Direction-less (no `-in`/`-out` suffix) + +```bash +officecli set deck.pptx /slide[N] --prop transition=circle +officecli set deck.pptx /slide[N] --prop transition=diamond +officecli set deck.pptx /slide[N] --prop transition=plus +officecli set deck.pptx /slide[N] --prop transition=wedge +``` + +These four are direction-less. Passing `-in`/`-out` is rejected with a +clear error rather than silently dropped: + +``` +Error: Transition 'circle' does not accept a direction modifier (got '-in'). +'circle' is a direction-less shape transition — drop the suffix and +use plain 'transition=circle'. +``` + +### 2. In / Out + +```bash +officecli set deck.pptx /slide[N] --prop transition=zoom-in +officecli set deck.pptx /slide[N] --prop transition=zoom-out +officecli set deck.pptx /slide[N] --prop transition=box-in +officecli set deck.pptx /slide[N] --prop transition=box-out +``` + +The default is `-in`; bare `zoom` / `box` round-trip as `zoom` / `box` +(default collapses on readback), `zoom-out` / `box-out` round-trip with +the suffix intact. + +**Box is a PowerPoint 2013+ "modern" transition.** officecli writes +it with an inline fade fallback baked in, so pre-2013 PowerPoint plays +a fade in its place instead of nothing. + +### 3. Spoke count — `wheel-N` + +```bash +officecli set deck.pptx /slide[N] --prop transition=wheel # 4 spokes (default) +officecli set deck.pptx /slide[N] --prop transition=wheel-1 # 1 spoke +officecli set deck.pptx /slide[N] --prop transition=wheel-8 # 8 spokes +``` + +The integer suffix (1..32) is the spoke count, not a duration. To set +both spokes and duration: combine — `wheel-8-1500` writes 8 spokes + +1500 ms duration. Readback returns `wheel-N` for non-default counts; +`wheel-4` collapses to bare `wheel`. + +## Related + +- [transitions-bands.md](transitions-bands.md) for split-vertical-in / split-horizontal-out (similar in/out modifier). +- [transitions-basic.md](transitions-basic.md) for the no-shape baseline (cut/fade/dissolve). diff --git a/examples/ppt/transitions/transitions-shapes.pptx b/examples/ppt/transitions/transitions-shapes.pptx new file mode 100644 index 000000000..075bb351b Binary files /dev/null and b/examples/ppt/transitions/transitions-shapes.pptx differ diff --git a/examples/ppt/transitions/transitions-shapes.sh b/examples/ppt/transitions/transitions-shapes.sh new file mode 100755 index 000000000..22d978f9f --- /dev/null +++ b/examples/ppt/transitions/transitions-shapes.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Shape-mask transitions — the new slide reveals through a growing geometric +# mask cut into the old slide. OOXML calls these CT_OptionalBlackTransition. +# +# Direction-less (NO -in/-out suffix; officecli will reject one): +# circle, diamond, plus, wedge +# +# Direction-ful (-in / -out): +# box, zoom +# +# Spoke-count (wheel-N where N = number of spokes, 1..8 typical): +# wheel-1, wheel-2, wheel-3, wheel-4 (default), wheel-8 +# +# Box is stored as PowerPoint 2013+ `` inside +# mc:AlternateContent (older PowerPoint plays the fallback fade). `box-in` +# is the default (no invX/invY); `box-out` flips both invX and invY. + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/transitions-shapes.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +n=0 +add_demo_slide() { + n=$((n+1)) + local trans=$1 title=$2 bg=$3 + officecli add "$PPTX" / --type slide + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill="$bg" + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop text="$title" --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm + if [ -n "$trans" ]; then officecli set "$PPTX" "/slide[$n]" --prop transition="$trans"; fi +} + +add_demo_slide "" "Shape Transitions" "1F3864" + +# Direction-less geometric masks +for t in circle diamond plus wedge; do + add_demo_slide "$t" "$t" "C00000" +done + +# In/out direction masks +for combo in zoom-in zoom-out box-in box-out; do + add_demo_slide "$combo" "$combo" "2E75B6" +done + +# Wheel spokes: same shape, different spoke count +for n_spokes in 1 2 3 4 8; do + add_demo_slide "wheel-$n_spokes" "wheel-$n_spokes ($n_spokes spokes)" "7030A0" +done + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/transitions/transitions-timing.md b/examples/ppt/transitions/transitions-timing.md new file mode 100644 index 000000000..666235784 --- /dev/null +++ b/examples/ppt/transitions/transitions-timing.md @@ -0,0 +1,77 @@ +# Transition Timing — Speed, Duration, and Advance Knobs + +Three files work together: + +- **transitions-timing.sh** — Build script. +- **transitions-timing.pptx** — 9-slide deck. +- **transitions-timing.md** — This file. + +## Regenerate + +```bash +cd examples/ppt/transitions +bash transitions-timing.sh +# → transitions-timing.pptx +``` + +## Four knobs + +### 1. Legacy speed token (PowerPoint 97+) + +```bash +officecli set deck.pptx /slide[N] --prop transition=fade-fast +officecli set deck.pptx /slide[N] --prop transition=fade-med # or 'medium' +officecli set deck.pptx /slide[N] --prop transition=fade-slow +``` + +`Get` surfaces the value as the read-only `transitionSpeed` format key. + +### 2. Office 2010+ duration in milliseconds + +```bash +officecli set deck.pptx /slide[N] --prop transition=fade-500 # 0.5 s +officecli set deck.pptx /slide[N] --prop transition=fade-1500 # 1.5 s +officecli set deck.pptx /slide[N] --prop transition=fade-3000 # 3.0 s +``` + +`Get` surfaces the value as the read-only `transitionDuration` format +key (millisecond integer). + +Specifying both speed and duration is allowed — newer PowerPoint +honors `@dur`, older falls back to `@spd`. + +### 3. Auto-advance after N milliseconds + +```bash +officecli set deck.pptx /slide[N] --prop transition=fade --prop advanceTime=2000 +# To clear later: +officecli set deck.pptx /slide[N] --prop advanceTime=none +``` + +When `advanceTime` is set, the slide moves on by itself after N ms in +Slide Show mode. The transition itself can be `cut` if you want an +instant auto-advance with no animation. + +### 4. Disable click-to-advance + +```bash +officecli set deck.pptx /slide[N] --prop transition=fade --prop advanceClick=false +``` + +`advanceClick` defaults to true (and the XML attribute is stripped +when true — only the explicit `false` survives a round-trip). With +`advanceClick=false`, the slide ignores click/Enter advance; the user +must use arrow keys or wait for `advanceTime`. + +## Round-trip semantics + +- `advanceClick=true` → XML attribute stripped → readback emits no + `advanceClick` key (default is true). +- `advanceClick=false` → XML keeps `advClick="0"` → readback emits + `advanceClick=false`. +- `advanceTime=none` → XML attribute removed → readback emits no + `advanceTime` key. + +## Related + +- [transitions-basic.md](transitions-basic.md) — uses `transition=none` to remove the entire transition element. diff --git a/examples/ppt/transitions/transitions-timing.pptx b/examples/ppt/transitions/transitions-timing.pptx new file mode 100644 index 000000000..2ea6a7391 Binary files /dev/null and b/examples/ppt/transitions/transitions-timing.pptx differ diff --git a/examples/ppt/transitions/transitions-timing.sh b/examples/ppt/transitions/transitions-timing.sh new file mode 100755 index 000000000..9e5e57b41 --- /dev/null +++ b/examples/ppt/transitions/transitions-timing.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# Transition timing — speed token vs millisecond duration, plus the +# auto-advance / click-to-advance knobs. +# +# Speed token (legacy CT_SlideTransition @spd, PowerPoint 97+): +# fast / medium|med / slow (e.g. fade-slow) +# +# Duration in ms (CT_TransitionStartSoundAction @dur extLst, Office 2010+): +# integer ms (e.g. fade-1500 → 1.5 seconds) +# +# Specifying both in the same combined token is allowed — the integer +# wins for new PowerPoint, the speed for legacy. Speed-only also writes +# transitionSpeed; duration-only writes transitionDuration on readback. +# +# Auto-advance: +# advanceTime= auto-advance after N milliseconds (or 'none' to clear) +# advanceClick=false disable click-to-advance (default true, stripped from XML when true) + +set -e + +DIR="$(dirname "$0")" +PPTX="$DIR/transitions-timing.pptx" + +rm -f "$PPTX" +officecli create "$PPTX" +officecli open "$PPTX" + +n=0 +add_demo_slide() { + n=$((n+1)) + local trans=$1 title=$2 bg=$3 + officecli add "$PPTX" / --type slide + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill="$bg" + officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop text="$title" --prop size=40 --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm + if [ -n "$trans" ]; then officecli set "$PPTX" "/slide[$n]" --prop transition="$trans"; fi +} + +add_demo_slide "" "Transition Timing" "1F3864" + +# Legacy speed tokens +add_demo_slide "fade-fast" "fade-fast (legacy @spd)" "C00000" +add_demo_slide "fade-med" "fade-med (legacy @spd)" "2E75B6" +add_demo_slide "fade-slow" "fade-slow (legacy @spd)" "7030A0" + +# Office 2010+ duration in ms +add_demo_slide "fade-500" "fade-500ms" "4F7C3A" +add_demo_slide "fade-1500" "fade-1500ms" "8A5A2B" +add_demo_slide "fade-3000" "fade-3000ms" "404040" + +# Auto-advance: slide stays for 2 seconds then advances on its own +n=$((n+1)) +officecli add "$PPTX" / --type slide +officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill=BF8F00 +officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop text="advanceTime=2000 (auto-advance after 2s)" --prop size=36 \ + --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm +officecli set "$PPTX" "/slide[$n]" --prop transition=fade --prop advanceTime=2000 + +# Disable click-to-advance: this slide will only advance via auto-time or arrow keys +n=$((n+1)) +officecli add "$PPTX" / --type slide +officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop x=0 --prop y=0 --prop width=33.87cm --prop height=19.05cm --prop fill=2E5C8A +officecli add "$PPTX" "/slide[$n]" --type shape \ + --prop text="advanceClick=false (no click advance)" --prop size=36 \ + --prop bold=true --prop color=FFFFFF --prop align=center \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=4cm +officecli set "$PPTX" "/slide[$n]" --prop transition=fade --prop advanceClick=false + +officecli close "$PPTX" +officecli validate "$PPTX" +echo "Created: $PPTX" diff --git a/examples/ppt/video.md b/examples/ppt/video.md new file mode 100644 index 000000000..3d0ab515a --- /dev/null +++ b/examples/ppt/video.md @@ -0,0 +1,6 @@ +# video + +TODO: rewrite script with annotated officecli commands. + +See [video.py](video.py) and [video.pptx](video.pptx). + diff --git a/examples/ppt/outputs/gen-video-pptx.pptx b/examples/ppt/video.pptx similarity index 100% rename from examples/ppt/outputs/gen-video-pptx.pptx rename to examples/ppt/video.pptx diff --git a/examples/ppt/gen-video-pptx.py b/examples/ppt/video.py similarity index 99% rename from examples/ppt/gen-video-pptx.py rename to examples/ppt/video.py index 38bd03cad..e88f64d5e 100755 --- a/examples/ppt/gen-video-pptx.py +++ b/examples/ppt/video.py @@ -124,7 +124,7 @@ def main(): run(f'set "{out_pptx}" /slide[2] --prop background=0D1B2A') run(f'set "{out_pptx}" /slide[2]/shape[1] --prop color=FFFFFF') run(f'add "{out_pptx}" /slide[2] --type video ' - f'--prop path="{video_path}" ' + f'--prop src="{video_path}" ' f'--prop poster="{cover_path}" ' f'--prop x=2cm --prop y=4cm --prop width=22cm --prop height=12.5cm ' f'--prop volume=80 --prop autoplay=true') diff --git a/examples/word/README.md b/examples/word/README.md deleted file mode 100644 index 1f95f990c..000000000 --- a/examples/word/README.md +++ /dev/null @@ -1,174 +0,0 @@ -# Word (.docx) Examples - -Examples demonstrating OfficeCLI capabilities for Word document automation. - -## 📄 Scripts - -### [gen-formulas.sh](gen-formulas.sh) -**Insert mathematical formulas and equations** - -```bash -bash gen-formulas.sh -``` - -**Demonstrates:** -- LaTeX math formula support -- Equation insertion -- Formula formatting - -**Output:** [`outputs/complex_formulas.docx`](outputs/complex_formulas.docx) - ---- - -### [gen-complex-tables.sh](gen-complex-tables.sh) -**Generate complex tables with styling** - -```bash -bash gen-complex-tables.sh -``` - -**Demonstrates:** -- Table creation and formatting -- Cell styling (borders, shading, alignment) -- Row and column manipulation -- Table properties (width, height, spacing) - -**Output:** [`outputs/complex_tables.docx`](outputs/complex_tables.docx) - ---- - -### [gen-complex-textbox.sh](gen-complex-textbox.sh) -**Create styled text boxes** - -```bash -bash gen-complex-textbox.sh -``` - -**Demonstrates:** -- Text box creation -- Font styling (bold, italic, size, color) -- Text alignment and formatting -- Paragraph properties - -**Output:** Generated dynamically - ---- - -## 🎓 Key Concepts - -### Document Structure -``` -/document - /body - /p[1] # Paragraph 1 - /r[1] # Run 1 - /p[2] - /tbl[1] # Table 1 - /tr[1] # Row 1 - /tc[1] # Cell 1 -``` - -### Common Commands - -**Create a paragraph:** -```bash -officecli add report.docx /body --type paragraph \ - --prop text="Hello World" \ - --prop style=Heading1 -``` - -**Modify text formatting:** -```bash -officecli set report.docx /body/p[1]/r[1] \ - --prop bold=true \ - --prop color=FF0000 \ - --prop size=24 -``` - -**Add a table:** -```bash -officecli add report.docx /body --type table \ - --prop rows=3 \ - --prop cols=4 -``` - ---- - -## 📊 Available Properties - -### Paragraph -- `text` - Paragraph text content -- `style` - Paragraph style (Normal, Heading1-9, etc.) -- `alignment` - left, center, right, justify -- `lineSpacing` - Line spacing (e.g., 1.5, 2.0) -- `indent` - Indentation in points - -### Run (Text Formatting) -- `text` - Text content -- `bold` - true/false -- `italic` - true/false -- `underline` - true/false -- `strike` - true/false -- `font` - Font name -- `size` - Font size in points -- `color` - Hex color (e.g., FF0000) -- `highlight` - Highlight color - -### Table -- `rows` - Number of rows -- `cols` - Number of columns -- `width` - Table width -- `border.color` - Border color -- `border.width` - Border width -- `border.style` - Border style - -**For complete property list:** -```bash -officecli docx set -officecli docx set paragraph -officecli docx set run -officecli docx set table -``` - ---- - -## 🔧 Tips - -1. **View structure first:** - ```bash - officecli view report.docx outline - ``` - -2. **Check content:** - ```bash - officecli view report.docx text - ``` - -3. **Query elements:** - ```bash - officecli query report.docx "paragraph[style=Heading1]" - ``` - -4. **Batch operations:** - ```bash - cat << EOF | officecli batch report.docx - [ - {"command":"add","parent":"/body","type":"paragraph","props":{"text":"Para 1"}}, - {"command":"add","parent":"/body","type":"paragraph","props":{"text":"Para 2"}} - ] - EOF - ``` - -5. **Validate after changes:** - ```bash - officecli validate report.docx - ``` - ---- - -## 📚 More Resources - -- [Complete Word documentation](../../SKILL.md#word-docx) -- [All examples](../) -- [PowerPoint examples](../powerpoint/) -- [Excel examples](../excel/) diff --git a/examples/word/formulas.docx b/examples/word/formulas.docx new file mode 100644 index 000000000..95d9a93b2 Binary files /dev/null and b/examples/word/formulas.docx differ diff --git a/examples/word/formulas.md b/examples/word/formulas.md new file mode 100644 index 000000000..28ac1c73b --- /dev/null +++ b/examples/word/formulas.md @@ -0,0 +1,6 @@ +# formulas + +TODO: rewrite script with annotated officecli commands. + +See [formulas.sh](formulas.sh) and [formulas.docx](formulas.docx). + diff --git a/examples/word/gen-formulas.sh b/examples/word/formulas.sh similarity index 57% rename from examples/word/gen-formulas.sh rename to examples/word/formulas.sh index ec14f06a4..2c6e4a33a 100755 --- a/examples/word/gen-formulas.sh +++ b/examples/word/formulas.sh @@ -3,14 +3,14 @@ # Usage: ./gen_formulas.sh [officecli path] CLI="${1:-officecli}" -OUT="$(dirname "$0")/complex_formulas.docx" +OUT="$(dirname "$0")/formulas.docx" rm -f "$OUT" $CLI create "$OUT" $CLI open "$OUT" # ==================== Title ==================== -$CLI add "$OUT" /body --type paragraph --prop text="Complex Math/Chemistry/Physics Formula Collection" --prop style=Heading1 --prop alignment=center +$CLI add "$OUT" /body --type paragraph --prop text="Complex Math/Chemistry/Physics Formula Collection" --prop style=Heading1 --prop align=center # ==================== I. Algebra ==================== $CLI add "$OUT" /body --type paragraph --prop text="I. Algebra" --prop style=Heading2 @@ -126,6 +126,84 @@ $CLI add "$OUT" /body --type equation --prop 'formula=B(\nu, T) = \frac{2h\nu^{3 $CLI add "$OUT" /body --type paragraph --prop text="30. Lorentz Transformation:" $CLI add "$OUT" /body --type equation --prop 'formula=t^{\prime} = \gamma \left(t - \frac{vx}{c^{2}}\right), \quad \gamma = \frac{1}{\sqrt{1 - \frac{v^{2}}{c^{2}}}}' +# ==================== VIII. Advanced Notation ==================== +$CLI add "$OUT" /body --type paragraph --prop text="VIII. Advanced Notation" --prop style=Heading2 + +$CLI add "$OUT" /body --type paragraph --prop text="31. Matrix (pmatrix):" +$CLI add "$OUT" /body --type equation --prop 'formula=A = \begin{pmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \end{pmatrix}' + +$CLI add "$OUT" /body --type paragraph --prop text="32. Determinant (vmatrix):" +$CLI add "$OUT" /body --type equation --prop 'formula=\det(A) = \begin{vmatrix} a & b \\ c & d \end{vmatrix} = ad - bc' + +$CLI add "$OUT" /body --type paragraph --prop text="33. Bracketed Matrix (bmatrix):" +$CLI add "$OUT" /body --type equation --prop 'formula=I_{3} = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}' + +$CLI add "$OUT" /body --type paragraph --prop text="34. Piecewise Function (cases):" +$CLI add "$OUT" /body --type equation --prop 'formula=|x| = \begin{cases} x, & x \geq 0 \\ -x, & x < 0 \end{cases}' + +$CLI add "$OUT" /body --type paragraph --prop text="35. Auto-sized Delimiters (various brackets):" +$CLI add "$OUT" /body --type equation --prop 'formula=\left[ \frac{a}{b} \right] + \left\{ \frac{c}{d} \right\} + \left| \frac{e}{f} \right| + \left\langle \frac{g}{h} \right\rangle' + +$CLI add "$OUT" /body --type paragraph --prop text="36. Floor and Ceiling:" +$CLI add "$OUT" /body --type equation --prop 'formula=\left\lfloor \frac{n}{2} \right\rfloor + \left\lceil \frac{n}{2} \right\rceil = n' + +$CLI add "$OUT" /body --type paragraph --prop text="37. Underbrace and Overbrace:" +$CLI add "$OUT" /body --type equation --prop 'formula=\underbrace{1 + 2 + \cdots + n}_{n \text{ terms}} = \overbrace{\frac{n(n+1)}{2}}^{\text{closed form}}' + +$CLI add "$OUT" /body --type paragraph --prop text="38. Overset (definition):" +$CLI add "$OUT" /body --type equation --prop 'formula=f(x) \overset{\text{def}}{=} \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}' + +$CLI add "$OUT" /body --type paragraph --prop text="39. Math Fonts (mathbb / mathcal / mathbf / mathrm):" +$CLI add "$OUT" /body --type equation --prop 'formula=\forall x \in \mathbb{R}, \exists \mathcal{L} : \mathbf{v} \mapsto \mathrm{d}\mathbf{v}' + +$CLI add "$OUT" /body --type paragraph --prop text="40. Cancellation:" +$CLI add "$OUT" /body --type equation --prop 'formula=\frac{(x+1) \cancel{(x-1)}}{\cancel{(x-1)}} = x + 1' + +$CLI add "$OUT" /body --type paragraph --prop text="41. Cancel-to (limit):" +$CLI add "$OUT" /body --type equation --prop 'formula=\lim_{x \to \infty} \cancelto{0}{\frac{1}{x}} + 1 = 1' + +$CLI add "$OUT" /body --type paragraph --prop text="42. Boxed Result:" +$CLI add "$OUT" /body --type equation --prop 'formula=\boxed{E = mc^{2}}' + +$CLI add "$OUT" /body --type paragraph --prop text="43. Accents (bar / vec / tilde / ddot):" +$CLI add "$OUT" /body --type equation --prop 'formula=\bar{x} = \frac{1}{n} \sum x_{i}, \quad \vec{F} = m\ddot{\vec{r}}, \quad \tilde{f}(\xi)' + +$CLI add "$OUT" /body --type paragraph --prop text="44. Overline and Underline:" +$CLI add "$OUT" /body --type equation --prop 'formula=\overline{A \cup B} = \overline{A} \cap \overline{B}, \quad \underline{x} \leq x \leq \overline{x}' + +$CLI add "$OUT" /body --type paragraph --prop text="45. Hyperbolic and Inverse Trig:" +$CLI add "$OUT" /body --type equation --prop 'formula=\arctan(x) = \int_{0}^{x} \frac{dt}{1+t^{2}}, \quad \cosh^{2}(x) - \sinh^{2}(x) = 1' + +$CLI add "$OUT" /body --type paragraph --prop text="46. Custom Operator (operatorname):" +$CLI add "$OUT" /body --type equation --prop 'formula=\operatorname{lcm}(a, b) \cdot \gcd(a, b) = |ab|' + +$CLI add "$OUT" /body --type paragraph --prop text="47. Modular Arithmetic:" +$CLI add "$OUT" /body --type equation --prop 'formula=a \equiv b \pmod{n} \iff n \mid (a - b), \quad 17 \bmod 5 = 2' + +$CLI add "$OUT" /body --type paragraph --prop text="48. Double Integral with Text:" +$CLI add "$OUT" /body --type equation --prop 'formula=\iint_{D} f(x,y) \, dA \quad \text{where } D = \{(x,y) : x^{2}+y^{2} \leq 1\}' + +$CLI add "$OUT" /body --type paragraph --prop text="49. Big Operators (bigcup / bigcap / coprod):" +$CLI add "$OUT" /body --type equation --prop 'formula=\bigcup_{i=1}^{n} A_{i} \supseteq \bigcap_{i=1}^{n} A_{i}, \quad \coprod_{i \in I} X_{i}' + +$CLI add "$OUT" /body --type paragraph --prop text="50. Greek Letters (full uppercase set):" +$CLI add "$OUT" /body --type equation --prop 'formula=\Gamma, \Theta, \Xi, \Pi, \Phi, \Psi, \Omega \in \{\alpha, \beta, \gamma, \delta, \epsilon, \zeta, \eta, \theta\}' + +$CLI add "$OUT" /body --type paragraph --prop text="51. Dots (ldots / cdots / vdots / ddots):" +$CLI add "$OUT" /body --type equation --prop 'formula=M = \begin{pmatrix} a_{11} & \cdots & a_{1n} \\ \vdots & \ddots & \vdots \\ a_{m1} & \cdots & a_{mn} \end{pmatrix}, \quad x_{1}, x_{2}, \ldots, x_{n}' + +$CLI add "$OUT" /body --type paragraph --prop text="52. Spacing Control (quad / qquad / thinsp):" +$CLI add "$OUT" /body --type equation --prop 'formula=a + b \, c \; d \quad e \qquad f' + +$CLI add "$OUT" /body --type paragraph --prop text="53. Colored Math (textcolor / color):" +$CLI add "$OUT" /body --type equation --prop 'formula=\textcolor{red}{x^{2}} + \textcolor{blue}{2xy} + \textcolor{green}{y^{2}} = \color{purple}{(x+y)^{2}}' + +$CLI add "$OUT" /body --type paragraph --prop text="54. Set Theory:" +$CLI add "$OUT" /body --type equation --prop 'formula=A \subseteq B \iff \forall x \in A, x \in B; \quad A \setminus B = \{x : x \in A \land x \notin B\}; \quad \emptyset \subset A' + +$CLI add "$OUT" /body --type paragraph --prop text="55. Norm and Inner Product:" +$CLI add "$OUT" /body --type equation --prop 'formula=\|x\|_{2} = \sqrt{\langle x, x \rangle} = \sqrt{\sum_{i=1}^{n} x_{i}^{2}}' + $CLI close "$OUT" echo "Generated: $OUT" diff --git a/examples/word/numbering-showcase.docx b/examples/word/numbering-showcase.docx new file mode 100644 index 000000000..e1a13e22a Binary files /dev/null and b/examples/word/numbering-showcase.docx differ diff --git a/examples/word/numbering-showcase.md b/examples/word/numbering-showcase.md new file mode 100644 index 000000000..5f5d3ab9a --- /dev/null +++ b/examples/word/numbering-showcase.md @@ -0,0 +1,7 @@ +# numbering-showcase + +TODO: rewrite script with annotated officecli commands. + +See [numbering-showcase.sh](numbering-showcase.sh) and [numbering-showcase.docx](numbering-showcase.docx). + +--- diff --git a/examples/word/numbering-showcase.sh b/examples/word/numbering-showcase.sh new file mode 100755 index 000000000..ffeabe513 --- /dev/null +++ b/examples/word/numbering-showcase.sh @@ -0,0 +1,294 @@ +#!/bin/bash +# numbering-showcase.sh — exercise every supported `num` / `abstractNum` feature. +# +# Builds a single .docx that demonstrates: +# • abstractNum top-level props: name, styleLink, numStyleLink, multiLevelType +# • Per-level dotted props on all 9 levels: format, text, start, indent, +# hanging, justification, suff, font, size, color, bold, italic +# • num mode A (auto-create matching abstractNum from format/text/indent) +# • num mode B (reuse existing abstractNum via abstractNumId) +# • num mode C (lvlOverride.start — restart numbering for one instance) +# • Two num instances sharing one abstractNum → independent counters +# • style-borne numPr (Heading-style multi-level outline) +# • Set on /numbering/abstractNum[@id=N]/level[L] after creation +set -e + +DOCX="$(dirname "$0")/numbering-showcase.docx" +echo "Building $DOCX ..." +rm -f "$DOCX" +officecli create "$DOCX" + +# ============================================================ +# Title +# ============================================================ +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Numbering & List Showcase" --prop align=center \ + --prop bold=true --prop size=20 + +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Generated by officecli — covers abstractNum, num, and style-borne numPr" \ + --prop align=center --prop italic=true --prop color=666666 + +officecli add "$DOCX" /body --type paragraph --prop "text=" + +# ============================================================ +# Section 1: abstractNum #100 — fully customized 3-level numbered template +# Level 0: decimal, red bold, 14pt — "1." +# Level 1: lowerLetter, blue italic — "a)" +# Level 2: lowerRoman gray — "i." +# Levels 3-8: auto-fallback cycle +# ============================================================ +officecli add "$DOCX" /body --type paragraph \ + --prop "text=1. Three-level numbered list (custom marker styling)" \ + --prop bold=true --prop size=14 --prop spaceBefore=240 --prop spaceAfter=120 + +officecli add "$DOCX" /numbering --type abstractNum \ + --prop id=100 \ + --prop "name=ShowcaseMultilevel" \ + --prop type=hybridMultilevel \ + --prop "level0.format=decimal" \ + --prop "level0.text=%1." \ + --prop "level0.indent=720" \ + --prop "level0.hanging=360" \ + --prop "level0.justification=left" \ + --prop "level0.suff=tab" \ + --prop "level0.color=C00000" \ + --prop "level0.bold=true" \ + --prop "level0.size=14" \ + --prop "level1.format=lowerLetter" \ + --prop "level1.text=%2)" \ + --prop "level1.indent=1440" \ + --prop "level1.hanging=360" \ + --prop "level1.color=2E74B5" \ + --prop "level1.italic=true" \ + --prop "level2.format=lowerRoman" \ + --prop "level2.text=%3." \ + --prop "level2.indent=2160" \ + --prop "level2.hanging=360" \ + --prop "level2.color=666666" + +# A num instance pointing at #100 +NUMID_A=$(officecli add "$DOCX" /numbering --type num --prop abstractNumId=100 \ + | sed -n 's|.*@id=\([0-9]*\)\].*|\1|p') +echo " Created num #$NUMID_A → abstractNum #100" + +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Project Phoenix kickoff agenda" \ + --prop "numId=$NUMID_A" --prop ilvl=0 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Stakeholder alignment" \ + --prop "numId=$NUMID_A" --prop ilvl=1 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=identify decision makers" \ + --prop "numId=$NUMID_A" --prop ilvl=2 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=schedule discovery interviews" \ + --prop "numId=$NUMID_A" --prop ilvl=2 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Architecture review" \ + --prop "numId=$NUMID_A" --prop ilvl=1 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Sprint planning" \ + --prop "numId=$NUMID_A" --prop ilvl=0 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Resource allocation" \ + --prop "numId=$NUMID_A" --prop ilvl=0 + +# ============================================================ +# Section 2: Independent counters (default) vs Word-style continuation +# Two new nums on the same abstractNum: by default each gets its own +# auto-injected startOverride.0 → counters are independent. The third +# num passes --prop continue=true to opt into Word's literal "continue +# from previous num" behavior. +# ============================================================ +officecli add "$DOCX" /body --type paragraph --prop "text=" +officecli add "$DOCX" /body --type paragraph \ + --prop "text=2. Independent counters (default) and continue=true opt-in" \ + --prop bold=true --prop size=14 --prop spaceBefore=240 --prop spaceAfter=120 + +NUMID_B=$(officecli add "$DOCX" /numbering --type num --prop abstractNumId=100 \ + | sed -n 's|.*@id=\([0-9]*\)\].*|\1|p') +echo " Created num #$NUMID_B → independent counter (auto-injected startOverride.0=1)" + +NUMID_CONT=$(officecli add "$DOCX" /numbering --type num \ + --prop abstractNumId=100 --prop continue=true \ + | sed -n 's|.*@id=\([0-9]*\)\].*|\1|p') +echo " Created num #$NUMID_CONT → Word-style continuation (continue=true)" + +officecli add "$DOCX" /body --type paragraph \ + --prop "text=List B starts fresh at 1 (default behavior)" \ + --prop "numId=$NUMID_B" --prop ilvl=0 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=List B item two (counts 2)" \ + --prop "numId=$NUMID_B" --prop ilvl=0 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=List C continues from List A's count (continue=true)" \ + --prop "numId=$NUMID_CONT" --prop ilvl=0 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=List C item two" \ + --prop "numId=$NUMID_CONT" --prop ilvl=0 + +# ============================================================ +# Section 3: Mode C — num with lvlOverride.start (restart at 100) +# ============================================================ +officecli add "$DOCX" /body --type paragraph --prop "text=" +officecli add "$DOCX" /body --type paragraph \ + --prop "text=3. Restart numbering with startOverride" \ + --prop bold=true --prop size=14 --prop spaceBefore=240 --prop spaceAfter=120 + +NUMID_C=$(officecli add "$DOCX" /numbering --type num \ + --prop abstractNumId=100 --prop start=100 \ + | sed -n 's|.*@id=\([0-9]*\)\].*|\1|p') +echo " Created num #$NUMID_C → abstractNum #100 with startOverride.0=100" + +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Numbered starting from 100" \ + --prop "numId=$NUMID_C" --prop ilvl=0 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Continues from 101" \ + --prop "numId=$NUMID_C" --prop ilvl=0 + +# ============================================================ +# Section 4: Bullet list with custom glyphs and font color +# ============================================================ +officecli add "$DOCX" /body --type paragraph --prop "text=" +officecli add "$DOCX" /body --type paragraph \ + --prop "text=4. Custom-styled bullet list (★ / ▶ / ●)" \ + --prop bold=true --prop size=14 --prop spaceBefore=240 --prop spaceAfter=120 + +officecli add "$DOCX" /numbering --type abstractNum \ + --prop id=200 \ + --prop "name=StarBullet" \ + --prop type=hybridMultilevel \ + --prop "level0.format=bullet" --prop "level0.text=★" \ + --prop "level0.color=E8B003" --prop "level0.size=12" \ + --prop "level1.format=bullet" --prop "level1.text=▶" \ + --prop "level1.font=Arial" \ + --prop "level1.color=2E74B5" --prop "level1.indent=1440" \ + --prop "level2.format=bullet" --prop "level2.text=●" \ + --prop "level2.color=70AD47" --prop "level2.indent=2160" + +NUMID_BULLET=$(officecli add "$DOCX" /numbering --type num --prop abstractNumId=200 \ + | sed -n 's|.*@id=\([0-9]*\)\].*|\1|p') + +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Top-level milestone" \ + --prop "numId=$NUMID_BULLET" --prop ilvl=0 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Sub-milestone with deliverable" \ + --prop "numId=$NUMID_BULLET" --prop ilvl=1 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Nitty-gritty detail" \ + --prop "numId=$NUMID_BULLET" --prop ilvl=2 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Another top-level milestone" \ + --prop "numId=$NUMID_BULLET" --prop ilvl=0 + +# ============================================================ +# Section 5: Mode A — num auto-creates abstractNum on the fly +# ============================================================ +officecli add "$DOCX" /body --type paragraph --prop "text=" +officecli add "$DOCX" /body --type paragraph \ + --prop "text=5. Mode A — num auto-creates abstractNum" \ + --prop bold=true --prop size=14 --prop spaceBefore=240 --prop spaceAfter=120 + +NUMID_AUTO=$(officecli add "$DOCX" /numbering --type num \ + --prop "level0.format=upperRoman" --prop "level0.text=%1." \ + --prop "level0.indent=720" --prop "level0.size=12" \ + --prop "level0.color=7030A0" --prop "level0.bold=true" \ + | sed -n 's|.*@id=\([0-9]*\)\].*|\1|p') +echo " Mode A created num #$NUMID_AUTO + matching abstractNum" + +officecli add "$DOCX" /body --type paragraph \ + --prop "text=The first part of the proposal" \ + --prop "numId=$NUMID_AUTO" --prop ilvl=0 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=The second part" \ + --prop "numId=$NUMID_AUTO" --prop ilvl=0 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=The third part" \ + --prop "numId=$NUMID_AUTO" --prop ilvl=0 + +# ============================================================ +# Section 6: Style-borne numPr — paragraphs inherit numbering via pStyle +# ============================================================ +officecli add "$DOCX" /body --type paragraph --prop "text=" +officecli add "$DOCX" /body --type paragraph \ + --prop "text=6. Style-borne numbering (paragraph inherits via pStyle)" \ + --prop bold=true --prop size=14 --prop spaceBefore=240 --prop spaceAfter=120 + +# Build a dedicated abstractNum + num for this style +officecli add "$DOCX" /numbering --type abstractNum \ + --prop id=300 --prop "name=StyleBorne" \ + --prop "level0.format=decimalZero" --prop "level0.text=%1." \ + --prop "level0.indent=720" --prop "level0.color=C00000" + +NUMID_STYLE=$(officecli add "$DOCX" /numbering --type num --prop abstractNumId=300 \ + | sed -n 's|.*@id=\([0-9]*\)\].*|\1|p') + +# Style holds the numPr; paragraphs reference the style without their own numId +officecli add "$DOCX" /styles --type style \ + --prop id=ShowcaseListItem \ + --prop "name=Showcase List Item" \ + --prop type=paragraph \ + --prop basedOn=Normal \ + --prop "numId=$NUMID_STYLE" --prop ilvl=0 + +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Inherits numbering through style" \ + --prop style=ShowcaseListItem +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Second item, also via style" \ + --prop style=ShowcaseListItem +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Third item — note: paragraphs themselves have no numPr" \ + --prop style=ShowcaseListItem + +# ============================================================ +# Section 7: Set after create — modify abstractNum #100 level 3 +# ============================================================ +officecli add "$DOCX" /body --type paragraph --prop "text=" +officecli add "$DOCX" /body --type paragraph \ + --prop "text=7. Modify abstractNum after creation" \ + --prop bold=true --prop size=14 --prop spaceBefore=240 --prop spaceAfter=120 + +# Override level 3 (deepest reached in section 1) with green color + larger size +officecli set "$DOCX" '/numbering/abstractNum[@id=100]/level[3]' \ + --prop format=decimal --prop "text=Step %4 ⇒" \ + --prop color=70AD47 --prop bold=true --prop size=12 + +NUMID_DEEP=$(officecli add "$DOCX" /numbering --type num --prop abstractNumId=100 \ + | sed -n 's|.*@id=\([0-9]*\)\].*|\1|p') + +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Outer step" \ + --prop "numId=$NUMID_DEEP" --prop ilvl=0 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Mid step" \ + --prop "numId=$NUMID_DEEP" --prop ilvl=1 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Inner step" \ + --prop "numId=$NUMID_DEEP" --prop ilvl=2 +officecli add "$DOCX" /body --type paragraph \ + --prop "text=Deepest step (modified after creation)" \ + --prop "numId=$NUMID_DEEP" --prop ilvl=3 + +# ============================================================ +# Closer +# ============================================================ +officecli add "$DOCX" /body --type paragraph --prop "text=" +officecli add "$DOCX" /body --type paragraph \ + --prop "text=End of showcase. Open in Word/Google Docs to see all numbering rendered." \ + --prop italic=true --prop color=666666 --prop align=center + +officecli close "$DOCX" +echo "" +echo "Done. Output: $DOCX" +echo "" +echo "Summary of generated definitions:" +officecli query "$DOCX" /numbering 2>/dev/null | head -5 || true +echo "" +echo " abstractNum #100 (ShowcaseMultilevel) — used by num #$NUMID_A, #$NUMID_B, #$NUMID_C, #$NUMID_DEEP" +echo " abstractNum #200 (StarBullet) — used by num #$NUMID_BULLET" +echo " abstractNum #300 (StyleBorne) — used by num #$NUMID_STYLE (via ShowcaseListItem style)" +echo " abstractNum #auto — used by num #$NUMID_AUTO (mode A)" diff --git a/examples/word/outputs/complex_formulas.docx b/examples/word/outputs/complex_formulas.docx deleted file mode 100644 index 085110a2e..000000000 Binary files a/examples/word/outputs/complex_formulas.docx and /dev/null differ diff --git a/examples/word/outputs/complex_tables.docx b/examples/word/outputs/complex_tables.docx deleted file mode 100644 index c488860ae..000000000 Binary files a/examples/word/outputs/complex_tables.docx and /dev/null differ diff --git a/examples/word/revisions.docx b/examples/word/revisions.docx new file mode 100644 index 000000000..32691273e Binary files /dev/null and b/examples/word/revisions.docx differ diff --git a/examples/word/revisions.md b/examples/word/revisions.md new file mode 100644 index 000000000..b374b4fea --- /dev/null +++ b/examples/word/revisions.md @@ -0,0 +1,84 @@ +# revisions + +End-to-end demo of the docx revision (track-changes) API, covering **every** revision element the docx handler supports on this branch. Run [revisions.sh](revisions.sh) to regenerate [revisions.docx](revisions.docx); open the result in Word and you will see all categories of marker in the review pane. + +## Coverage matrix + +| Section | Scope | OOXML element | `revision.type` in `get` | Created via | +|--------:|-----------|-----------------------------------|---------------------------------|--------------------------------------------------------------------------------------| +| 1a | run | `w:ins` | `insertion` | `set /body/p[N]/r[M] --prop revision.type=ins --prop revision.author=…` | +| 1b | run | `w:del` | `deletion` | `set /body/p[N]/r[M] --prop revision.type=del --prop revision.author=…` | +| 1c | run | `w:rPrChange` (implicit) | `formatChange` | `set /body/p[N]/r[M] --prop font.color=… --prop revision.author=…` | +| 1d | run | `w:rPrChange` (explicit) | `formatChange` | `set /body/p[N]/r[M] --prop revision.type=format --prop italic=true --prop revision.author=…` | +| 2a | paragraph | `w:ins` + `paragraphMarkInsertion`| `insertion` + `paragraphMarkInsertion` | `add /body --type paragraph --prop text=… --prop revision.author=…` | +| 2b | paragraph | `w:del` + `paragraphMarkDeletion` | `deletion` + `paragraphMarkDeletion` | `remove /body/p[N] --prop revision.author=…` | +| 2c | paragraph | `w:pPrChange` | `paragraphChange` | `set /body/p[N] --prop align=… --prop revision.author=…` | +| 3 | run | `w:moveFrom` / `w:moveTo` (paired)| `moveFrom` / `moveTo` | two `set` calls with the **same** `revision.id=N` | +| 4a | table | `w:tblPrChange` | `tableChange` | `set /body/tbl[N] --prop style=… --prop revision.author=…` | +| 4b | row | `w:trPrChange` | `rowChange` | `set /body/tbl[N]/tr[N] --prop height=… --prop revision.author=…` | +| 4c | cell | `w:tcPrChange` (+ cascades) | `cellChange` | `set /body/tbl[N]/tr[N]/tc[N] --prop shd=… --prop revision.author=…` | +| 4d | cell | `w:tcPr/w:cellIns` | `cellInsertion` | `add /body/tbl[N]/tr[N] --type cell --prop revision.author=…` | +| 4e | cell | `w:tcPr/w:cellDel` | `cellDeletion` | `remove /body/tbl[N]/tr[N]/tc[N] --prop revision.author=…` | +| 4f | row | `w:trPr/w:ins` | `rowInsertion` | `add /body/tbl[N] --type row --prop revision.author=…` | +| 5 | section | `w:sectPrChange` | `sectionChange` | `set /body/sectPr[N] --prop pageWidth=… --prop revision.author=…` (note: `/body/sectPr[N]`, **not** `/section[N]`) | +| 6a | run | default-author fallback | `insertion` author=`OfficeCLI` | `set … --prop revision.author=""` (empty string → `"OfficeCLI"`; only on `set`, not `add`) | +| 6b | paragraph | explicit `revision.id` | `insertion` id=9001 | `add … --prop revision.author=Grace --prop revision.id=9001` | + +Side-effects worth knowing about (not separately addressable, but generated automatically): + +- Cell `tcPrChange` cascades `w:tblPrExChange` per row + `w:tblGridChange` when grid columns mutate. Mac Word needs both for correct rendering; the handler emits them with unique revision ids — see commits `caf03ab2`, `b489826f`, `b271629f`. +- Move pairs always emit Range markers (`w:moveFromRangeStart/End`, `w:moveToRangeStart/End`) coalesced over contiguous runs — see `5e79cf8f`, `0f1f9a3a`. + +## The two `revision.*` namespaces + +Disjoint by design — mixing them throws. + +- **Create** namespace: `revision.type` + `revision.author` + `revision.date` + `revision.id`, supplied on the host element (`add` or `set` on a run / paragraph / table / row / cell / section). +- **Action** namespace: `revision.action=accept|reject`, supplied on a `/revision[…]` selector or a native DOM path. +- A property change combined with `revision.author` (no `.type`, no `.action`) is the format-change path — writes the new value and captures the previous `rPr` / `pPr` / `tcPr` / `trPr` / `tblPr` / `sectPr` snapshot in the matching `*Change` element. +- Legacy `trackChange.*` keys were renamed to `revision.*` on this branch (`d9e812f3`). Bare `revision=…` is no longer accepted. + +## Addressing markers + +`query revision` returns one node per `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `w:*PrChange` / `w:*Ins` / `w:*Del`. The `Path` column is: + +- `/revision[@id=N]` — **stable** across save (use this in scripts). +- `/revision[@id=N][@type=moveFrom|moveTo]` — single-end disambiguator for a move pair. `set /revision[@id=N]` without the `[@type=…]` segment still acts on **both** halves together (pair-wise accept/reject). +- `/revision[N]` — positional fallback for markers with no `w:id`. Indices shift after each accept/reject, so prefer `@id=`. + +The `nativePath` column gives you the underlying DOM path (`/body/p[N]/ins[M]`, `/body/tbl[1]/tr[2]/tc[2]`, `/body/sectPr`, …), which works as a `set` target for `revision.action=…` too. + +## Accept / reject syntax (all forms used in the demo) + +```bash +# All +officecli set revisions.docx /revision --prop revision.action=accept +officecli set revisions.docx /revision --prop revision.action=reject + +# By author / type +officecli set revisions.docx '/revision[@author=Alice]' --prop revision.action=accept +officecli set revisions.docx '/revision[@type=ins]' --prop revision.action=accept +officecli set revisions.docx '/revision[@type=del]' --prop revision.action=reject + +# By stable id (positional indices shift; ids do not) +officecli set revisions.docx '/revision[@id=42]' --prop revision.action=accept +officecli set revisions.docx '/revision[@id=42][@type=moveTo]' --prop revision.action=reject + +# Or via the native DOM path +officecli set revisions.docx '/body/p[2]/ins[1]' --prop revision.action=accept +officecli set revisions.docx '/body/tbl[1]/tr[2]/tc[2]' --prop revision.action=accept +``` + +The script runs the accept/reject demo on a *temp copy* so the shipped `revisions.docx` keeps every marker in place for inspection. + +## Notes + +- `revision.date` accepts ISO-8601 (`2026-05-25T10:00:00Z`); omitted ⇒ `DateTime.UtcNow` at write time. +- `revision.author=""` on `set` falls back to `"OfficeCLI"`. On `add`, the empty author means "do not track" (the paragraph is added plain) — use `set` if you want the fallback behavior. +- `revision.id` is auto-allocated from the shared paraId pool when omitted. Supply explicitly to: + - pair `moveFrom`/`moveTo` (must match), + - get a deterministic id when downstream post-processing needs it (section 6b). +- The body's section properties path is **`/body/sectPr[N]`**, not `/section[N]`. `/section[N]` exists for mid-document sections (each `` carrying a `pPr/sectPr`); the final body-level section lives on the body's trailing `sectPr` element. +- `revisionNumber` on `/` (document save counter) and `trackChanges` on `/settings` (the "Track Changes" mode toggle) are **separate** properties — neither creates per-edit markers. + +See `officecli help docx revision` for the full property reference. diff --git a/examples/word/revisions.sh b/examples/word/revisions.sh new file mode 100755 index 000000000..6bfafac20 --- /dev/null +++ b/examples/word/revisions.sh @@ -0,0 +1,481 @@ +#!/bin/bash +# Generate a Word tracked-revision showcase document covering every +# revision element the docx handler supports on this branch. +# +# Marker creation (covered): +# Run scope: +# * w:ins set + revision.type=ins +# * w:del set + revision.type=del +# * w:rPrChange (implicit) set + font.* / bold / ... + revision.author +# * w:rPrChange (explicit) set + revision.type=format + +# * w:moveFrom / w:moveTo set + revision.type=moveFrom|moveTo + shared revision.id +# Paragraph scope: +# * w:ins (paragraph) add paragraph + revision.author +# * w:del (paragraph) remove paragraph + revision.author +# * w:pPrChange set paragraph prop (align/indent/...) + revision.author +# Table scope: +# * w:tblPrChange set tbl + style/... + revision.author +# * w:trPrChange set tr + height/header/... + revision.author +# * w:tcPrChange set tc + shd/borders/... + revision.author +# (implicitly cascades w:tblPrExChange + w:tblGridChange when grid mutates) +# * w:trPr/w:ins (rowInsertion) add row + revision.author +# * w:tcPr/w:cellIns add cell + revision.author +# * w:tcPr/w:cellDel remove cell + revision.author +# Section scope: +# * w:sectPrChange set /body/sectPr[N] + revision.author +# Bonus: +# * Default author (revision.author="" -> "OfficeCLI") +# * Auto-allocated revision.id (omit; comes from shared paraId pool) +# * Explicit revision.id (required for move pair; allowed everywhere) +# +# Action verb demo (on a temp copy at the bottom): +# * /revision[@author=NAME] accept/reject by author +# * /revision[@type=ins|del|...] accept/reject by type +# * /revision[@id=N] accept/reject by stable id +# * /revision[@id=N][@type=moveTo] single-end of a move pair +# * native path (/body/p[N]/ins[M] ...) accept/reject in DOM terms +# * /revision accept-all / reject-all (terminal sweep) + +set -e + +DIR="$(dirname "$0")" +DOCX="$DIR/revisions.docx" + +echo "==========================================" +echo "Generating tracked-revision showcase: $DOCX" +echo "==========================================" + +rm -f "$DOCX" +officecli create "$DOCX" +officecli open "$DOCX" + +# -------------------------------------------------------------------------- +# Paragraph index map (1-based; counts EVERY body paragraph). Comments inline +# below show what each set targets so the indices stay correct as the doc +# grows. Tracked-delete keeps the paragraph element in place (wrapped in +# w:del), so subsequent indices do NOT shift after step 2b. +# -------------------------------------------------------------------------- + +# p[1] title, p[2] spacer +officecli add "$DOCX" /body --type paragraph --prop text="Revision API — Full Coverage" --prop style=Heading1 --prop align=center +officecli add "$DOCX" /body --type paragraph --prop text="" + +# ========================================================================== +# Section 1 — Run-level edits. +# p[3] H2, p[4]=ins target, p[5]=del target, +# p[6]=implicit-format target, p[7]=explicit-format target +# ========================================================================== +echo " -> Section 1: run-level edits (ins / del / rPrChange implicit + explicit)" +officecli add "$DOCX" /body --type paragraph --prop text="1. Run-level edits" --prop style=Heading2 +officecli add "$DOCX" /body --type paragraph --prop text="This run will be marked as an INSERTION." +officecli add "$DOCX" /body --type paragraph --prop text="This run will be marked as a DELETION." +officecli add "$DOCX" /body --type paragraph --prop text="This run keeps the text and gets an IMPLICIT format change (font.color)." +officecli add "$DOCX" /body --type paragraph --prop text="This run gets an EXPLICIT revision.type=format with italic toggle." + +# 1a. w:ins around the run. +officecli set "$DOCX" '/body/p[4]/r[1]' \ + --prop revision.type=ins \ + --prop revision.author=Alice \ + --prop revision.date=2026-05-25T10:00:00Z + +# 1b. w:del around the run (text becomes w:delText). +officecli set "$DOCX" '/body/p[5]/r[1]' \ + --prop revision.type=del \ + --prop revision.author=Bob \ + --prop revision.date=2026-05-25T10:05:00Z + +# 1c. Implicit format change — any font.* prop + revision.author captures the +# previous rPr in w:rPrChange. Most natural form. +officecli set "$DOCX" '/body/p[6]/r[1]' \ + --prop font.color=C00000 \ + --prop bold=true \ + --prop revision.author=Carol \ + --prop revision.date=2026-05-25T10:10:00Z + +# 1d. Explicit revision.type=format. Still needs a real property change +# alongside (empty rPrChange records nothing, so the handler errors out). +officecli set "$DOCX" '/body/p[7]/r[1]' \ + --prop revision.type=format \ + --prop italic=true \ + --prop revision.author=Carol \ + --prop revision.date=2026-05-25T10:11:00Z + +# ========================================================================== +# Section 2 — Paragraph-level edits. +# p[8] H2 +# p[9] whole-paragraph tracked insertion (add + revision.author) +# p[10] plain paragraph that becomes a tracked deletion (remove + ...) +# p[11] paragraph that gets a pPrChange (set align + revision.author) +# ========================================================================== +echo " -> Section 2: paragraph-level edits (ins / del / pPrChange)" +officecli add "$DOCX" /body --type paragraph --prop text="2. Paragraph-level edits" --prop style=Heading2 + +# 2a. w:ins around the entire paragraph (plus paragraphMarkInsertion on the ¶). +officecli add "$DOCX" /body --type paragraph \ + --prop text="This whole paragraph was inserted by Alice as a tracked change." \ + --prop revision.author=Alice \ + --prop revision.date=2026-05-25T10:15:00Z + +# 2b. w:del around the entire paragraph (plus paragraphMarkDeletion). +# remove + revision.author KEEPS the element (wraps it); it does not drop it. +officecli add "$DOCX" /body --type paragraph --prop text="This whole paragraph will be tracked-deleted by Bob." +officecli remove "$DOCX" '/body/p[10]' \ + --prop revision.author=Bob \ + --prop revision.date=2026-05-25T10:20:00Z + +# 2c. pPrChange — set a paragraph-level property (alignment here) + revision.author. +# Surfaces in query as revision.type=paragraphChange. +officecli add "$DOCX" /body --type paragraph --prop text="This paragraph had alignment changed (pPrChange) by Carol." +officecli set "$DOCX" '/body/p[11]' \ + --prop align=center \ + --prop revision.author=Carol \ + --prop revision.date=2026-05-25T10:21:00Z + +# ========================================================================== +# Section 3 — Paired move (shared revision.id binds the two halves). +# p[12] H2, p[13]=moveFrom source, p[14]=moveTo destination +# ========================================================================== +echo " -> Section 3: paired move (moveFrom + moveTo, shared id)" +officecli add "$DOCX" /body --type paragraph --prop text="3. Moved content" --prop style=Heading2 +officecli add "$DOCX" /body --type paragraph --prop text="Source: this sentence is being relocated." +officecli add "$DOCX" /body --type paragraph --prop text="Destination: it will land here in its new home." + +# revision.id MUST be supplied (and equal) for the two halves to pair. +officecli set "$DOCX" '/body/p[13]/r[1]' \ + --prop revision.type=moveFrom \ + --prop revision.author=Alice \ + --prop revision.date=2026-05-25T10:25:00Z \ + --prop revision.id=500 +officecli set "$DOCX" '/body/p[14]/r[1]' \ + --prop revision.type=moveTo \ + --prop revision.author=Alice \ + --prop revision.date=2026-05-25T10:25:00Z \ + --prop revision.id=500 + +# ========================================================================== +# Section 4 — Table-scope revisions (all five table elements). +# p[15] H2, tbl[1] = 3 rows x 3 cols seed. +# Order of operations is chosen so per-row/per-cell indices stay correct. +# ========================================================================== +echo " -> Section 4: table scope (tblPrChange + trPrChange + tcPrChange + row/cell ins/del)" +officecli add "$DOCX" /body --type paragraph --prop text="4. Table-scope revisions" --prop style=Heading2 +officecli add "$DOCX" /body --type table --prop rows=3 --prop cols=3 + +# Seed content +officecli set "$DOCX" '/body/tbl[1]/tr[1]/tc[1]' --prop text="Header A" --prop bold=true +officecli set "$DOCX" '/body/tbl[1]/tr[1]/tc[2]' --prop text="Header B" --prop bold=true +officecli set "$DOCX" '/body/tbl[1]/tr[1]/tc[3]' --prop text="Header C" --prop bold=true +officecli set "$DOCX" '/body/tbl[1]/tr[2]/tc[1]' --prop text="row2 a" +officecli set "$DOCX" '/body/tbl[1]/tr[2]/tc[2]' --prop text="row2 b (shading change)" +officecli set "$DOCX" '/body/tbl[1]/tr[2]/tc[3]' --prop text="row2 c" +officecli set "$DOCX" '/body/tbl[1]/tr[3]/tc[1]' --prop text="row3 a (cell delete)" +officecli set "$DOCX" '/body/tbl[1]/tr[3]/tc[2]' --prop text="row3 b" +officecli set "$DOCX" '/body/tbl[1]/tr[3]/tc[3]' --prop text="row3 c" + +# 4a. tblPrChange — table-level property change. +officecli set "$DOCX" '/body/tbl[1]' \ + --prop style=TableGrid \ + --prop revision.author=Dan \ + --prop revision.date=2026-05-25T10:30:00Z + +# 4b. trPrChange — row-level property change (row height). +officecli set "$DOCX" '/body/tbl[1]/tr[1]' \ + --prop height=600 \ + --prop revision.author=Dan \ + --prop revision.date=2026-05-25T10:31:00Z + +# 4c. tcPrChange — cell-level property change (shading). +# Cascades tblPrExChange / tblGridChange automatically when needed. +officecli set "$DOCX" '/body/tbl[1]/tr[2]/tc[2]' \ + --prop shd=FFE699 \ + --prop revision.author=Dan \ + --prop revision.date=2026-05-25T10:32:00Z + +# 4d. Cell insertion — add a 4th cell to row 2. +officecli add "$DOCX" '/body/tbl[1]/tr[2]' --type cell \ + --prop text="row2 d (inserted)" \ + --prop revision.author=Eve \ + --prop revision.date=2026-05-25T10:33:00Z + +# 4e. Cell deletion — drop cell 1 of row 3 (tracked, not destructive). +officecli remove "$DOCX" '/body/tbl[1]/tr[3]/tc[1]' \ + --prop revision.author=Eve \ + --prop revision.date=2026-05-25T10:34:00Z + +# 4f. Row insertion — append a row at the table tail; whole row marked inserted. +officecli add "$DOCX" '/body/tbl[1]' --type row \ + --prop revision.author=Eve \ + --prop revision.date=2026-05-25T10:35:00Z + +# ========================================================================== +# Section 5 — Section properties (sectPrChange). +# The body's section properties live at /body/sectPr[1] (NOT /body/sect[1]). +# ========================================================================== +echo " -> Section 5: section properties (sectPrChange)" +officecli add "$DOCX" /body --type paragraph --prop text="5. Section properties" --prop style=Heading2 +officecli add "$DOCX" /body --type paragraph --prop text="The body sectPr below got a tracked pageWidth change." + +officecli set "$DOCX" '/body/sectPr[1]' \ + --prop pageWidth=11906 \ + --prop revision.author=Frank \ + --prop revision.date=2026-05-25T10:40:00Z + +# ========================================================================== +# Section 6 — Defaults & explicit-id (bonus). +# ========================================================================== +echo " -> Section 6: default author + auto-allocated id" +officecli add "$DOCX" /body --type paragraph --prop text="6. Defaults" --prop style=Heading2 + +# 6a. Empty revision.author -> falls back to "OfficeCLI". +# `add + revision.author=""` silently produces an untracked paragraph +# (empty author = "no revision"); the default-author fallback fires on +# the `set` path. So we add the paragraph plain, then `set` it with an +# empty author to demonstrate the fallback. +officecli add "$DOCX" /body --type paragraph \ + --prop text="This run was wrapped via set with revision.author=\"\" (defaults to OfficeCLI)." +officecli set "$DOCX" '/body/p[19]/r[1]' \ + --prop revision.type=ins \ + --prop revision.author="" \ + --prop revision.date=2026-05-25T10:44:00Z + +# 6b. Explicit revision.id outside of a move pair — accepted, you control the +# w:id attribute. Useful when post-processing needs a deterministic id. +officecli add "$DOCX" /body \ + --type paragraph \ + --prop text="This paragraph carries an explicit revision.id=9001." \ + --prop revision.author=Grace \ + --prop revision.date=2026-05-25T10:45:00Z \ + --prop revision.id=9001 + +# ========================================================================== +# Section 7 — Find + Replace combined with revision tracking. +# Mirrors Word's Find&Replace dialog with Track Changes ON: every match is +# wrapped in the marker shape inferred from the props you pass alongside. +# The handler auto-allocates a fresh revision.id per marker (one w:del per +# matched run + one w:ins for the replacement, or one w:rPrChange per match, +# etc.), so `--prop revision.id=…` is rejected on find — would collide. +# ========================================================================== +echo " -> Section 7: find + revision (Word-style Find&Replace with Track Changes)" +officecli add "$DOCX" /body --type paragraph --prop text="7. Find + Replace + Revision" --prop style=Heading2 + +# Helper: add a paragraph and echo the path the handler assigned (e.g. +# /body/p[@paraId=00100012]) so subsequent `set --prop find=…` calls don't +# need to hand-count positional indices. The handler-assigned paraId path +# is stable across content shifts in the body, unlike /body/p[N] which +# drifts every time the section count above changes. +add_para_capture() { + officecli add "$DOCX" /body --type paragraph --prop text="$1" 2>&1 \ + | grep -oE '/body/p\[@paraId=[A-F0-9]+\]' | tail -1 +} + +# 7a. find + replace + revision via REGEX — track only the FIRST occurrence +# of "fox", leave subsequent ones alone. The bare `find=fox` would match +# every occurrence; controlling which match to track is a job for the +# regex pattern. +# +# Pattern: (? Section 8: find variants (replace='' delete-only + paragraph prop pPrChange)" +officecli add "$DOCX" /body --type paragraph --prop text="8. Find variants" --prop style=Heading2 + +# 8a. find + replace="" + revision — tracked DELETION of every match, no insertion. +# Useful for "scrub this token from the doc but keep an audit trail". +P8A=$(add_para_capture "8a. Remove the OBSOLETE token here. (delete-only via find — no insertion)") +officecli set "$DOCX" "$P8A" \ + --prop find=OBSOLETE \ + --prop replace= \ + --prop revision.author=Mira \ + --prop revision.date=2026-05-25T10:54:00Z + +# 8b. find + paragraph prop + revision — one w:pPrChange per matched paragraph. +# Same code path as `set /body/p[N] --prop align=… --prop revision.author=…`, +# but filtered to ONLY the paragraphs whose text actually matched the find. +P8B=$(add_para_capture "8b. This paragraph contains MARK so its alignment gets tracked-centered.") +officecli set "$DOCX" "$P8B" \ + --prop find=MARK \ + --prop align=center \ + --prop revision.author=Nora \ + --prop revision.date=2026-05-25T10:55:00Z + +officecli close "$DOCX" + +# ========================================================================== +# Inspection — list every revision marker in the shipped file. +# ========================================================================== +echo "" +echo "==========================================" +echo "All revisions in $DOCX:" +echo "==========================================" +officecli query "$DOCX" revision + +# ========================================================================== +# Action verbs — runs on a TEMP COPY so the shipped artifact keeps every +# marker intact for inspection in Word. +# ========================================================================== +DEMO="$(mktemp -t revisions-demo.XXXXXX).docx" +cp "$DOCX" "$DEMO" + +echo "" +echo "==========================================" +echo "Accept/reject demo on temp copy:" +echo " $DEMO" +echo "==========================================" + +# A. Single-end addressing of a move pair: `/revision[@id=N][@type=…]` +# addresses ONE half of a shared-id pair. Section 3 created moveFrom + +# moveTo at id=500; reject only the moveTo half here (the moveFrom +# survives — useful when reviewing decides "keep the deletion at source, +# discard the insertion at destination"). Done BEFORE step B because B +# accepts everything Alice authored, including this move pair. +echo " A) single-end move reject: /revision[@id=500][@type=moveTo]" +officecli set "$DEMO" '/revision[@id=500][@type=moveTo]' --prop revision.action=reject 2>&1 | tail -1 +SURVIVING_MOVE=$(officecli query "$DEMO" revision --json 2>/dev/null | python3 -c " +import sys, json +d = json.load(sys.stdin) +for r in d['data']['results']: + if r.get('format',{}).get('revision.id') == '500': + print(r['format'].get('revision.type')) +") +echo " surviving half of id=500: ${SURVIVING_MOVE:-none} (expected: moveFrom)" + +# B. Accept everything Alice authored. +echo " B) accept by author: /revision[@author=Alice]" +officecli set "$DEMO" '/revision[@author=Alice]' --prop revision.action=accept + +# C. Reject every w:del-typed revision still left. +echo " C) reject by type: /revision[@type=del]" +officecli set "$DEMO" '/revision[@type=del]' --prop revision.action=reject + +# D. Accept Carol's explicit-format change by its stable id. +CAROL_FMT_ID=$(officecli query "$DEMO" revision --json 2>/dev/null | python3 -c " +import sys, json +d = json.load(sys.stdin) +for r in d['data']['results']: + f = r.get('format', {}) + if f.get('revision.author') == 'Carol' and f.get('revision.type') == 'formatChange': + print(f['revision.id']); break +") +if [ -n "$CAROL_FMT_ID" ]; then + echo " D) accept by stable id: /revision[@id=$CAROL_FMT_ID]" + officecli set "$DEMO" "/revision[@id=$CAROL_FMT_ID]" --prop revision.action=accept +fi + +# E. Accept a marker via its native DOM path. Pick the first surviving marker +# after steps A-D and feed its nativePath back to `set --prop revision.action=accept`. +NATIVE_PATH=$(officecli query "$DEMO" revision --json 2>/dev/null | python3 -c " +import sys, json +d = json.load(sys.stdin) +for r in d['data']['results']: + np = r.get('format', {}).get('revision.nativePath','') + if np: + print(np); break +") +if [ -n "$NATIVE_PATH" ]; then + echo " E) accept by native path: $NATIVE_PATH" + officecli set "$DEMO" "$NATIVE_PATH" --prop revision.action=accept +fi + +# F. Sweep — reject everything still pending. +echo " F) terminal sweep: /revision (reject-all)" +officecli set "$DEMO" /revision --prop revision.action=reject + +REMAINING=$(officecli query "$DEMO" revision 2>&1 | grep -c "^/revision" || true) +echo " remaining markers after sweep: $REMAINING (expected: 0)" + +rm -f "$DEMO" + +# ========================================================================== +# Read-side capabilities — agent-friendly JSON envelope, plain-text render, +# dump→batch round-trip (revision keys survive serialization, so a doc with +# tracked changes can be regenerated end-to-end via `batch --input`). +# ========================================================================== +echo "" +echo "==========================================" +echo "Read-side capabilities on $DOCX:" +echo "==========================================" + +echo " i) query revision --json (first 3 markers, agent-consumable):" +officecli query "$DOCX" revision --json 2>/dev/null | python3 -c " +import sys, json +d = json.load(sys.stdin) +print(f' matches={d[\"data\"][\"matches\"]}') +for r in d['data']['results'][:3]: + f = r['format'] + print(f' path={r[\"path\"]} type={f.get(\"revision.type\")} author={f.get(\"revision.author\")} text={repr(r.get(\"text\",\"\"))[:40]}') +" + +echo "" +echo " ii) view text — runs/paragraphs render with their current content" +echo " (inserted text shows, deleted text is suppressed; cf. Word's" +echo " 'All Markup' vs 'No Markup' view):" +officecli view "$DOCX" text 2>&1 | head -10 | sed 's/^/ /' + +echo "" +echo " iii) dump --format batch round-trip — revision creation keys survive" +echo " serialization so a tracked-change doc regenerates end-to-end via" +echo " 'batch --input '. Sample of revision keys in dump:" +officecli dump "$DOCX" / --format batch 2>/dev/null \ + | python3 -c " +import sys, json +batch = json.load(sys.stdin) +rev_steps = [s for s in batch + if any(k.startswith('revision.') for k in (s.get('props') or {}))] +print(f' total batch steps: {len(batch)}, steps carrying revision.* props: {len(rev_steps)}') +for s in rev_steps[:3]: + keys = [k for k in s['props'] if k.startswith('revision.')] + print(f' {s[\"command\"]:>6} {s.get(\"path\",s.get(\"parent\",\"\")):40} keys={keys}') +" + +echo "" +echo "Done: $DOCX" +ls -lh "$DOCX" diff --git a/examples/word/tables.docx b/examples/word/tables.docx new file mode 100644 index 000000000..793df56d6 Binary files /dev/null and b/examples/word/tables.docx differ diff --git a/examples/word/tables.md b/examples/word/tables.md new file mode 100644 index 000000000..7ad30d6c6 --- /dev/null +++ b/examples/word/tables.md @@ -0,0 +1,6 @@ +# tables + +TODO: rewrite script with annotated officecli commands. + +See [tables.sh](tables.sh) and [tables.docx](tables.docx). + diff --git a/examples/word/tables.pptx b/examples/word/tables.pptx new file mode 100644 index 000000000..58c68fb13 Binary files /dev/null and b/examples/word/tables.pptx differ diff --git a/examples/word/gen-complex-tables.sh b/examples/word/tables.sh similarity index 97% rename from examples/word/gen-complex-tables.sh rename to examples/word/tables.sh index b0cb68baa..11d2e74d2 100755 --- a/examples/word/gen-complex-tables.sh +++ b/examples/word/tables.sh @@ -7,18 +7,21 @@ set -e echo "Using CLI: officecli" +DIR="$(dirname "$0")" + ############################################################################### # 1. Word Complex Table Document ############################################################################### -DOCX="complex_tables.docx" +DOCX="$DIR/tables.docx" echo "" echo "==========================================" echo "Generating Word complex table document: $DOCX" echo "==========================================" +rm -f "$DOCX" officecli create "$DOCX" officecli open "$DOCX" -officecli add "$DOCX" /body --type paragraph --prop text="Complex Table Examples" --prop style=Heading1 --prop alignment=center +officecli add "$DOCX" /body --type paragraph --prop text="Complex Table Examples" --prop style=Heading1 --prop align=center officecli add "$DOCX" /body --type paragraph --prop text="" # -- Table 1: Project Progress Tracker (vertical merge vmerge) -- @@ -87,53 +90,53 @@ officecli add "$DOCX" /body --type table --prop rows=8 --prop cols=5 # Header row 1 - gridspan=2 automatically removes merged tc officecli set "$DOCX" '/body/tbl[2]/tr[1]/tc[1]' --prop text="Category" --prop bold=true --prop shd=2E75B6 --prop color=FFFFFF --prop vmerge=restart --prop valign=center officecli set "$DOCX" '/body/tbl[2]/tr[1]/tc[2]' --prop text="Line Item" --prop bold=true --prop shd=2E75B6 --prop color=FFFFFF --prop vmerge=restart --prop valign=center -officecli set "$DOCX" '/body/tbl[2]/tr[1]/tc[3]' --prop text="Amount (10K USD)" --prop bold=true --prop shd=2E75B6 --prop color=FFFFFF --prop gridspan=2 --prop alignment=center +officecli set "$DOCX" '/body/tbl[2]/tr[1]/tc[3]' --prop text="Amount (10K USD)" --prop bold=true --prop shd=2E75B6 --prop color=FFFFFF --prop gridspan=2 --prop align=center # gridspan=2 removed original tc[4], original tc[5] becomes tc[4] officecli set "$DOCX" '/body/tbl[2]/tr[1]/tc[4]' --prop text="Notes" --prop bold=true --prop shd=2E75B6 --prop color=FFFFFF --prop vmerge=restart --prop valign=center # Header row 2 officecli set "$DOCX" '/body/tbl[2]/tr[2]/tc[1]' --prop text="" --prop vmerge=continue --prop shd=2E75B6 officecli set "$DOCX" '/body/tbl[2]/tr[2]/tc[2]' --prop text="" --prop vmerge=continue --prop shd=2E75B6 -officecli set "$DOCX" '/body/tbl[2]/tr[2]/tc[3]' --prop text="Budget" --prop bold=true --prop shd=5B9BD5 --prop color=FFFFFF --prop alignment=center -officecli set "$DOCX" '/body/tbl[2]/tr[2]/tc[4]' --prop text="Actual" --prop bold=true --prop shd=5B9BD5 --prop color=FFFFFF --prop alignment=center +officecli set "$DOCX" '/body/tbl[2]/tr[2]/tc[3]' --prop text="Budget" --prop bold=true --prop shd=5B9BD5 --prop color=FFFFFF --prop align=center +officecli set "$DOCX" '/body/tbl[2]/tr[2]/tc[4]' --prop text="Actual" --prop bold=true --prop shd=5B9BD5 --prop color=FFFFFF --prop align=center officecli set "$DOCX" '/body/tbl[2]/tr[2]/tc[5]' --prop text="" --prop vmerge=continue --prop shd=2E75B6 # Revenue (merge 3 rows) officecli set "$DOCX" '/body/tbl[2]/tr[3]/tc[1]' --prop text="Revenue" --prop vmerge=restart --prop valign=center --prop shd=DEEAF6 --prop bold=true officecli set "$DOCX" '/body/tbl[2]/tr[3]/tc[2]' --prop text="Product Sales" -officecli set "$DOCX" '/body/tbl[2]/tr[3]/tc[3]' --prop text="500.00" --prop alignment=right -officecli set "$DOCX" '/body/tbl[2]/tr[3]/tc[4]' --prop text="523.50" --prop alignment=right --prop color=00B050 +officecli set "$DOCX" '/body/tbl[2]/tr[3]/tc[3]' --prop text="500.00" --prop align=right +officecli set "$DOCX" '/body/tbl[2]/tr[3]/tc[4]' --prop text="523.50" --prop align=right --prop color=00B050 officecli set "$DOCX" '/body/tbl[2]/tr[3]/tc[5]' --prop text="Exceeded" officecli set "$DOCX" '/body/tbl[2]/tr[4]/tc[1]' --prop text="" --prop vmerge=continue --prop shd=DEEAF6 officecli set "$DOCX" '/body/tbl[2]/tr[4]/tc[2]' --prop text="Consulting Services" -officecli set "$DOCX" '/body/tbl[2]/tr[4]/tc[3]' --prop text="200.00" --prop alignment=right -officecli set "$DOCX" '/body/tbl[2]/tr[4]/tc[4]' --prop text="185.30" --prop alignment=right --prop color=FF0000 +officecli set "$DOCX" '/body/tbl[2]/tr[4]/tc[3]' --prop text="200.00" --prop align=right +officecli set "$DOCX" '/body/tbl[2]/tr[4]/tc[4]' --prop text="185.30" --prop align=right --prop color=FF0000 officecli set "$DOCX" '/body/tbl[2]/tr[4]/tc[5]' --prop text="Below target" officecli set "$DOCX" '/body/tbl[2]/tr[5]/tc[1]' --prop text="" --prop vmerge=continue --prop shd=DEEAF6 officecli set "$DOCX" '/body/tbl[2]/tr[5]/tc[2]' --prop text="Tech Licensing" -officecli set "$DOCX" '/body/tbl[2]/tr[5]/tc[3]' --prop text="80.00" --prop alignment=right -officecli set "$DOCX" '/body/tbl[2]/tr[5]/tc[4]' --prop text="92.00" --prop alignment=right --prop color=00B050 +officecli set "$DOCX" '/body/tbl[2]/tr[5]/tc[3]' --prop text="80.00" --prop align=right +officecli set "$DOCX" '/body/tbl[2]/tr[5]/tc[4]' --prop text="92.00" --prop align=right --prop color=00B050 officecli set "$DOCX" '/body/tbl[2]/tr[5]/tc[5]' --prop text="New partners" # Expenses (merge 3 rows) officecli set "$DOCX" '/body/tbl[2]/tr[6]/tc[1]' --prop text="Expenses" --prop vmerge=restart --prop valign=center --prop shd=FFF2CC --prop bold=true officecli set "$DOCX" '/body/tbl[2]/tr[6]/tc[2]' --prop text="Labor Cost" -officecli set "$DOCX" '/body/tbl[2]/tr[6]/tc[3]' --prop text="320.00" --prop alignment=right -officecli set "$DOCX" '/body/tbl[2]/tr[6]/tc[4]' --prop text="335.00" --prop alignment=right --prop color=FF0000 +officecli set "$DOCX" '/body/tbl[2]/tr[6]/tc[3]' --prop text="320.00" --prop align=right +officecli set "$DOCX" '/body/tbl[2]/tr[6]/tc[4]' --prop text="335.00" --prop align=right --prop color=FF0000 officecli set "$DOCX" '/body/tbl[2]/tr[6]/tc[5]' --prop text="New hires" officecli set "$DOCX" '/body/tbl[2]/tr[7]/tc[1]' --prop text="" --prop vmerge=continue --prop shd=FFF2CC officecli set "$DOCX" '/body/tbl[2]/tr[7]/tc[2]' --prop text="Operating Expenses" -officecli set "$DOCX" '/body/tbl[2]/tr[7]/tc[3]' --prop text="150.00" --prop alignment=right -officecli set "$DOCX" '/body/tbl[2]/tr[7]/tc[4]' --prop text="142.80" --prop alignment=right --prop color=00B050 +officecli set "$DOCX" '/body/tbl[2]/tr[7]/tc[3]' --prop text="150.00" --prop align=right +officecli set "$DOCX" '/body/tbl[2]/tr[7]/tc[4]' --prop text="142.80" --prop align=right --prop color=00B050 officecli set "$DOCX" '/body/tbl[2]/tr[7]/tc[5]' --prop text="Cost savings" officecli set "$DOCX" '/body/tbl[2]/tr[8]/tc[1]' --prop text="" --prop vmerge=continue --prop shd=FFF2CC officecli set "$DOCX" '/body/tbl[2]/tr[8]/tc[2]' --prop text="R&D Investment" -officecli set "$DOCX" '/body/tbl[2]/tr[8]/tc[3]' --prop text="180.00" --prop alignment=right -officecli set "$DOCX" '/body/tbl[2]/tr[8]/tc[4]' --prop text="195.50" --prop alignment=right +officecli set "$DOCX" '/body/tbl[2]/tr[8]/tc[3]' --prop text="180.00" --prop align=right +officecli set "$DOCX" '/body/tbl[2]/tr[8]/tc[4]' --prop text="195.50" --prop align=right officecli set "$DOCX" '/body/tbl[2]/tr[8]/tc[5]' --prop text="Strategic investment" # -- Table 3: Skill Assessment Matrix (color heatmap) -- @@ -143,20 +146,20 @@ officecli add "$DOCX" /body --type paragraph --prop text="3. Skill Assessment Ma officecli add "$DOCX" /body --type table --prop rows=6 --prop cols=7 # Header -officecli set "$DOCX" '/body/tbl[3]/tr[1]/tc[1]' --prop text="Name/Skill" --prop bold=true --prop shd=002060 --prop color=FFFFFF --prop alignment=center +officecli set "$DOCX" '/body/tbl[3]/tr[1]/tc[1]' --prop text="Name/Skill" --prop bold=true --prop shd=002060 --prop color=FFFFFF --prop align=center for col_data in "2:Python" "3:Java" "4:Frontend" "5:Database" "6:DevOps" "7:AI/ML"; do col="${col_data%%:*}"; name="${col_data#*:}" - officecli set "$DOCX" "/body/tbl[3]/tr[1]/tc[$col]" --prop text="$name" --prop bold=true --prop shd=002060 --prop color=FFFFFF --prop alignment=center + officecli set "$DOCX" "/body/tbl[3]/tr[1]/tc[$col]" --prop text="$name" --prop bold=true --prop shd=002060 --prop color=FFFFFF --prop align=center done # Colors: Expert=00B050(dark green) Proficient=92D050(light green) Familiar=FFC000(yellow) Beginner=FF0000(red) fill_skill_row() { local row=$1 person=$2; shift 2 - officecli set "$DOCX" "/body/tbl[3]/tr[$row]/tc[1]" --prop text="$person" --prop bold=true --prop shd=D6DCE4 --prop alignment=center + officecli set "$DOCX" "/body/tbl[3]/tr[$row]/tc[1]" --prop text="$person" --prop bold=true --prop shd=D6DCE4 --prop align=center local col=2 for cell in "$@"; do local text="${cell%%:*}" color="${cell#*:}" - officecli set "$DOCX" "/body/tbl[3]/tr[$row]/tc[$col]" --prop text="$text" --prop shd="$color" --prop color=FFFFFF --prop alignment=center --prop bold=true + officecli set "$DOCX" "/body/tbl[3]/tr[$row]/tc[$col]" --prop text="$text" --prop shd="$color" --prop color=FFFFFF --prop align=center --prop bold=true ((col++)) done } @@ -172,12 +175,13 @@ echo " Done: Word document: $DOCX" ############################################################################### # 2. Excel Sales Report ############################################################################### -XLSX="sales_report.xlsx" +XLSX="$DIR/tables.xlsx" echo "" echo "==========================================" echo "Generating Excel sales report: $XLSX" echo "==========================================" +rm -f "$XLSX" officecli create "$XLSX" officecli open "$XLSX" @@ -277,12 +281,13 @@ echo " Done: Excel document: $XLSX" ############################################################################### # 3. PowerPoint Data Report ############################################################################### -PPTX="data_presentation.pptx" +PPTX="$DIR/tables.pptx" echo "" echo "==========================================" echo "Generating PowerPoint data report: $PPTX" echo "==========================================" +rm -f "$PPTX" officecli create "$PPTX" officecli open "$PPTX" diff --git a/examples/word/tables.xlsx b/examples/word/tables.xlsx new file mode 100644 index 000000000..2b15221a7 Binary files /dev/null and b/examples/word/tables.xlsx differ diff --git a/examples/word/textbox.docx b/examples/word/textbox.docx new file mode 100644 index 000000000..cedd31b36 Binary files /dev/null and b/examples/word/textbox.docx differ diff --git a/examples/word/textbox.md b/examples/word/textbox.md new file mode 100644 index 000000000..4837da834 --- /dev/null +++ b/examples/word/textbox.md @@ -0,0 +1,6 @@ +# textbox + +TODO: rewrite script with annotated officecli commands. + +See [textbox.sh](textbox.sh) and [textbox.docx](textbox.docx). + diff --git a/examples/word/gen-complex-textbox.sh b/examples/word/textbox.sh similarity index 95% rename from examples/word/gen-complex-textbox.sh rename to examples/word/textbox.sh index e1240772c..8aab90320 100755 --- a/examples/word/gen-complex-textbox.sh +++ b/examples/word/textbox.sh @@ -4,20 +4,21 @@ set -e -OUT="complex-textbox-test.docx" +OUT="$(dirname "$0")/textbox.docx" echo "Using CLI: officecli" echo "Output file: $OUT" # ==================== Create base document ==================== +rm -f "$OUT" officecli create "$OUT" -officecli add "$OUT" /body --type paragraph --prop text="Complex Textbox Examples" --prop style=Heading1 --prop alignment=center +officecli add "$OUT" /body --type paragraph --prop text="Complex Textbox Examples" --prop style=Heading1 --prop align=center officecli add "$OUT" /body --type paragraph --prop text="The following contains multiple complex textbox scenarios for testing textbox behavior under various conditions." # ==================== Scenario 1: Basic Textbox (with border and background + VML Fallback) ==================== officecli add "$OUT" /body --type paragraph --prop text="Scenario 1: Basic Textbox (with border and background)" --prop style=Heading2 -officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' +officecli raw-set "$OUT" /document --xpath "//w:body/w:sectPr" --action insertbefore --xml ' @@ -57,14 +58,15 @@ officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' - + - + - + Basic Textbox (VML fallback) + @@ -77,7 +79,7 @@ echo "Done: Scenario 1: Basic Textbox" # ==================== Scenario 2: Multi-paragraph Rich Text Textbox ==================== officecli add "$OUT" /body --type paragraph --prop text="Scenario 2: Multi-paragraph Rich Text Textbox" --prop style=Heading2 -officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' +officecli raw-set "$OUT" /document --xpath "//w:body/w:sectPr" --action insertbefore --xml ' @@ -127,7 +129,7 @@ echo "Done: Scenario 2: Rich Text Textbox" # ==================== Scenario 3: Textbox with Nested Table ==================== officecli add "$OUT" /body --type paragraph --prop text="Scenario 3: Textbox with Nested Table" --prop style=Heading2 -officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' +officecli raw-set "$OUT" /document --xpath "//w:body/w:sectPr" --action insertbefore --xml ' @@ -204,7 +206,7 @@ echo "Done: Scenario 3: Nested Table" # ==================== Scenario 4: Rotated Textbox (45 degrees + gradient background) ==================== officecli add "$OUT" /body --type paragraph --prop text="Scenario 4: Rotated Textbox (45 degrees)" --prop style=Heading2 -officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' +officecli raw-set "$OUT" /document --xpath "//w:body/w:sectPr" --action insertbefore --xml ' @@ -256,7 +258,7 @@ echo "Done: Scenario 4: Rotated Textbox" # ==================== Scenario 5: Vertical Text Textbox ==================== officecli add "$OUT" /body --type paragraph --prop text="Scenario 5: Vertical Text Textbox" --prop style=Heading2 -officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' +officecli raw-set "$OUT" /document --xpath "//w:body/w:sectPr" --action insertbefore --xml ' @@ -302,7 +304,7 @@ echo "Done: Scenario 5: Vertical Textbox" # ==================== Scenario 6: Rounded Rectangle + Shadow ==================== officecli add "$OUT" /body --type paragraph --prop text="Scenario 6: Rounded Rectangle Textbox" --prop style=Heading2 -officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' +officecli raw-set "$OUT" /document --xpath "//w:body/w:sectPr" --action insertbefore --xml ' @@ -355,7 +357,7 @@ echo "Done: Scenario 6: Rounded Rectangle" # ==================== Scenario 7: Side-by-side Textboxes (Card Layout) ==================== officecli add "$OUT" /body --type paragraph --prop text="Scenario 7: Side-by-side Textboxes (Card Layout)" --prop style=Heading2 -officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' +officecli raw-set "$OUT" /document --xpath "//w:body/w:sectPr" --action insertbefore --xml ' @@ -481,7 +483,7 @@ echo "Done: Scenario 7: Side-by-side Cards" # ==================== Scenario 8: Borderless Transparent Textbox ==================== officecli add "$OUT" /body --type paragraph --prop text="Scenario 8: Borderless Transparent Textbox" --prop style=Heading2 -officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' +officecli raw-set "$OUT" /document --xpath "//w:body/w:sectPr" --action insertbefore --xml ' @@ -527,7 +529,7 @@ echo "Done: Scenario 8: Transparent Textbox" # ==================== Scenario 9: Text Overflow Textbox ==================== officecli add "$OUT" /body --type paragraph --prop text="Scenario 9: Text Overflow Textbox" --prop style=Heading2 -officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' +officecli raw-set "$OUT" /document --xpath "//w:body/w:sectPr" --action insertbefore --xml ' @@ -578,7 +580,7 @@ echo "Done: Scenario 9: Overflow Textbox" # ==================== Scenario 10: Textbox Stacking (Z-order) ==================== officecli add "$OUT" /body --type paragraph --prop text="Scenario 10: Textbox Stacking (Z-order)" --prop style=Heading2 -officecli raw-set "$OUT" /document --xpath "//w:body" --action append --xml ' +officecli raw-set "$OUT" /document --xpath "//w:body/w:sectPr" --action insertbefore --xml ' diff --git a/install.ps1 b/install.ps1 index 0ab96b72a..36ac2862c 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,21 +1,41 @@ -$repo = "iOfficeAI/OfficeCli" +$repo = "iOfficeAI/OfficeCLI" $asset = "officecli-win-x64.exe" $binary = "officecli.exe" +# Mirror primary, github fallback. The mirror is exercised first so issues +# surface there fast; github is the safety net when CF or the mirror is +# unreachable. +$mirrorBase = "https://d.officecli.ai" +$githubReleaseBase = "https://github.com/$repo/releases/latest/download" +$githubRawBase = "https://raw.githubusercontent.com/$repo/main" + +function Fetch-WithFallback { + param([string]$Primary, [string]$Fallback, [string]$OutFile) + try { + Invoke-WebRequest -Uri $Primary -OutFile $OutFile -TimeoutSec 30 -ErrorAction Stop + Write-Host " (via mirror)" + return $true + } catch { + Write-Host " mirror unreachable, falling back to github..." + try { + Invoke-WebRequest -Uri $Fallback -OutFile $OutFile -TimeoutSec 300 -ErrorAction Stop + return $true + } catch { + return $false + } + } +} + $source = $null -# Step 1: Try downloading from GitHub -$url = "https://github.com/$repo/releases/latest/download/$asset" -$checksumUrl = "https://github.com/$repo/releases/latest/download/SHA256SUMS" +# Step 1: Try downloading (mirror first, github fallback) $tempFile = "$env:TEMP\$binary" -Write-Host "Downloading OfficeCli..." -try { - Invoke-WebRequest -Uri $url -OutFile $tempFile +Write-Host "Downloading OfficeCLI..." +if (Fetch-WithFallback "$mirrorBase/releases/latest/download/$asset" "$githubReleaseBase/$asset" $tempFile) { # Verify checksum if available $checksumOk = $false - try { - $checksumFile = "$env:TEMP\officecli-SHA256SUMS" - Invoke-WebRequest -Uri $checksumUrl -OutFile $checksumFile + $checksumFile = "$env:TEMP\officecli-SHA256SUMS" + if (Fetch-WithFallback "$mirrorBase/releases/latest/download/SHA256SUMS" "$githubReleaseBase/SHA256SUMS" $checksumFile) { $checksumContent = Get-Content $checksumFile $expectedLine = $checksumContent | Where-Object { $_ -match $asset } if ($expectedLine) { @@ -31,7 +51,7 @@ try { } } Remove-Item -Force $checksumFile -ErrorAction SilentlyContinue - } catch { + } else { Write-Host "Checksum file not available, skipping verification." } $output = & $tempFile --version 2>&1 @@ -39,10 +59,10 @@ try { $source = $tempFile Write-Host "Download verified." } else { - Write-Host "Downloaded file is not a valid OfficeCli binary." + Write-Host "Downloaded file is not a valid OfficeCLI binary." Remove-Item -Force $tempFile -ErrorAction SilentlyContinue } -} catch { +} else { Write-Host "Download failed." } @@ -63,7 +83,7 @@ if (-not $source) { } if (-not $source) { - Write-Host "Error: Could not find a valid OfficeCli binary." + Write-Host "Error: Could not find a valid OfficeCLI binary." Write-Host "Download manually from: https://github.com/$repo/releases" exit 1 } @@ -74,7 +94,7 @@ if ($existing) { $installDir = Split-Path $existing.Source Write-Host "Found existing installation at $($existing.Source), upgrading..." } else { - $installDir = "$env:LOCALAPPDATA\OfficeCli" + $installDir = "$env:LOCALAPPDATA\OfficeCLI" } New-Item -ItemType Directory -Force -Path $installDir | Out-Null @@ -103,6 +123,7 @@ if (-not (Test-Path $skillMarker)) { "$env:USERPROFILE\.openclaw" = "OpenClaw" "$env:USERPROFILE\.nanobot\workspace" = "NanoBot" "$env:USERPROFILE\.zeroclaw\workspace" = "ZeroClaw" + "$env:USERPROFILE\.hermes" = "Hermes Agent" } foreach ($dir in $tools.Keys) { if (Test-Path $dir) { @@ -114,18 +135,17 @@ if (-not (Test-Path $skillMarker)) { if ($skillTargets.Count -gt 0) { Write-Host "Downloading officecli skill..." $tempSkill = "$env:TEMP\officecli-skill.md" - try { - Invoke-WebRequest -Uri "https://raw.githubusercontent.com/$repo/main/SKILL.md" -OutFile $tempSkill + if (Fetch-WithFallback "$mirrorBase/SKILL.md" "$githubRawBase/SKILL.md" $tempSkill) { foreach ($target in $skillTargets) { New-Item -ItemType Directory -Force -Path $target | Out-Null Copy-Item -Force $tempSkill "$target\SKILL.md" Write-Host " Installed: $target\SKILL.md" } Remove-Item -Force $tempSkill -ErrorAction SilentlyContinue - } catch {} + } } New-Item -ItemType File -Force -Path $skillMarker | Out-Null } -Write-Host "OfficeCli installed successfully!" +Write-Host "OfficeCLI installed successfully!" Write-Host "Run 'officecli --help' to get started." diff --git a/install.sh b/install.sh index c83052d43..2dbb10f73 100755 --- a/install.sh +++ b/install.sh @@ -1,9 +1,29 @@ #!/bin/bash set -e -REPO="iOfficeAI/OfficeCli" +REPO="iOfficeAI/OfficeCLI" BINARY_NAME="officecli" +# Mirror primary, github fallback. The mirror is exercised first so issues +# surface there fast; github is the final safety net. +MIRROR_BASE="https://d.officecli.ai" +GITHUB_RELEASE_BASE="https://github.com/$REPO/releases/latest/download" +GITHUB_RAW_BASE="https://raw.githubusercontent.com/$REPO/main" + +# fetch_with_fallback +# Returns 0 if either source delivered the file, non-zero if both failed. +# Short connect-timeout on primary so a dead mirror doesn't add minutes +# of stall before falling through. +fetch_with_fallback() { + local primary="$1" fallback="$2" out="$3" + if curl -fsSL --max-time 300 --connect-timeout 5 "$primary" -o "$out" 2>/dev/null; then + echo " (via mirror)" + return 0 + fi + echo " mirror unreachable, falling back to github..." + curl -fsSL --max-time 300 "$fallback" -o "$out" 2>/dev/null +} + # Detect platform OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) @@ -51,14 +71,18 @@ esac SOURCE="" -# Step 1: Try downloading from GitHub -DOWNLOAD_URL="https://github.com/$REPO/releases/latest/download/$ASSET" -CHECKSUM_URL="https://github.com/$REPO/releases/latest/download/SHA256SUMS" -echo "Downloading OfficeCli ($ASSET)..." -if curl -fsSL "$DOWNLOAD_URL" -o "/tmp/$BINARY_NAME" 2>/dev/null; then +# Step 1: Try downloading (mirror first, github fallback) +echo "Downloading OfficeCLI ($ASSET)..." +if fetch_with_fallback \ + "$MIRROR_BASE/releases/latest/download/$ASSET" \ + "$GITHUB_RELEASE_BASE/$ASSET" \ + "/tmp/$BINARY_NAME"; then # Verify checksum if available CHECKSUM_OK=false - if curl -fsSL "$CHECKSUM_URL" -o "/tmp/officecli-SHA256SUMS" 2>/dev/null; then + if fetch_with_fallback \ + "$MIRROR_BASE/releases/latest/download/SHA256SUMS" \ + "$GITHUB_RELEASE_BASE/SHA256SUMS" \ + "/tmp/officecli-SHA256SUMS"; then EXPECTED=$(grep "$ASSET" "/tmp/officecli-SHA256SUMS" | awk '{print $1}') if [ -n "$EXPECTED" ]; then if command -v sha256sum >/dev/null 2>&1; then @@ -104,7 +128,7 @@ if [ -z "$SOURCE" ]; then fi if [ -z "$SOURCE" ]; then - echo "Error: Could not find a valid OfficeCli binary." + echo "Error: Could not find a valid OfficeCLI binary." echo "Download manually from: https://github.com/$REPO/releases" exit 1 fi @@ -119,15 +143,22 @@ else fi mkdir -p "$INSTALL_DIR" -cp "$SOURCE" "$INSTALL_DIR/$BINARY_NAME" -chmod +x "$INSTALL_DIR/$BINARY_NAME" +# Atomic replace: stage as .new alongside the target, sign there, then rename. +# Overwriting the binary in place would trash the text segment of any +# running officecli process (macOS does not block ETXTBSY), leaving it +# stuck in uninterruptible `UE` state on the next code page fault. +cp "$SOURCE" "$INSTALL_DIR/$BINARY_NAME.new" +chmod +x "$INSTALL_DIR/$BINARY_NAME.new" # macOS: remove quarantine flag and ad-hoc codesign (required by AppleSystemPolicy) +# Done on the staged .new copy so the live binary is never mutated in place. if [ "$(uname -s)" = "Darwin" ]; then - xattr -d com.apple.quarantine "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || true - codesign -s - -f "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || true + xattr -d com.apple.quarantine "$INSTALL_DIR/$BINARY_NAME.new" 2>/dev/null || true + codesign -s - -f "$INSTALL_DIR/$BINARY_NAME.new" 2>/dev/null || true fi +mv -f "$INSTALL_DIR/$BINARY_NAME.new" "$INSTALL_DIR/$BINARY_NAME" + # Auto-add to PATH if needed case ":$PATH:" in *":$INSTALL_DIR:"*) ;; @@ -155,7 +186,7 @@ rm -f "/tmp/$BINARY_NAME" SKILL_MARKER="$INSTALL_DIR/.officecli-skills-installed" if [ ! -f "$SKILL_MARKER" ]; then SKILL_TARGETS="" - for tool_dir in "$HOME/.claude:Claude Code" "$HOME/.copilot:GitHub Copilot" "$HOME/.agents:Codex CLI" "$HOME/.cursor:Cursor" "$HOME/.windsurf:Windsurf" "$HOME/.minimax:MiniMax CLI" "$HOME/.openclaw:OpenClaw" "$HOME/.nanobot/workspace:NanoBot" "$HOME/.zeroclaw/workspace:ZeroClaw"; do + for tool_dir in "$HOME/.claude:Claude Code" "$HOME/.copilot:GitHub Copilot" "$HOME/.agents:Codex CLI" "$HOME/.cursor:Cursor" "$HOME/.windsurf:Windsurf" "$HOME/.minimax:MiniMax CLI" "$HOME/.openclaw:OpenClaw" "$HOME/.nanobot/workspace:NanoBot" "$HOME/.zeroclaw/workspace:ZeroClaw" "$HOME/.hermes:Hermes Agent"; do dir="${tool_dir%%:*}" name="${tool_dir##*:}" if [ -d "$dir" ]; then @@ -166,7 +197,10 @@ if [ ! -f "$SKILL_MARKER" ]; then if [ -n "$SKILL_TARGETS" ]; then echo "Downloading officecli skill..." - if curl -fsSL "https://raw.githubusercontent.com/$REPO/main/SKILL.md" -o "/tmp/officecli-skill.md" 2>/dev/null; then + if fetch_with_fallback \ + "$MIRROR_BASE/SKILL.md" \ + "$GITHUB_RAW_BASE/SKILL.md" \ + "/tmp/officecli-skill.md"; then for target in $SKILL_TARGETS; do mkdir -p "$target" cp "/tmp/officecli-skill.md" "$target/SKILL.md" @@ -178,5 +212,5 @@ if [ ! -f "$SKILL_MARKER" ]; then touch "$SKILL_MARKER" fi -echo "OfficeCli installed successfully!" +echo "OfficeCLI installed successfully!" echo "Run 'officecli --help' to get started." diff --git a/schemas/README.md b/schemas/README.md new file mode 100644 index 000000000..f67ec5271 --- /dev/null +++ b/schemas/README.md @@ -0,0 +1,28 @@ +# schemas/ + +Agent-facing capability schemas for officecli. Single source of truth for what the CLI supports, consumed in three places: + +1. **`officecli --help --json`** — runtime output for agents. Schemas are embedded into the binary at build time, so runtime does not depend on filesystem paths or network access. +2. **Contract tests** — every schema claim (`add`, `set`, `get`, `readback`) is verified against the real handler implementation. Properties marked `enforcement: strict` break CI on drift; `report` only log. +3. **Release-time wiki generation** (future) — wiki markdown is generated/diffed from schemas before publishing. During development, wiki is not touched; agents read schemas directly. + +## Layout + +``` +schemas/ + help/ + _schema.json ← JSON Schema (draft 2020-12) describing the format below + docx/.json ← Word per-element capability + pptx/.json ← PowerPoint per-element capability + xlsx/.json ← Excel per-element capability +``` + +## Editing rule + +Any PR that changes `Add`, `Set`, or `Get` behavior for an element **must** update the matching schema file in the same PR. CI contract tests will fail otherwise. + +## Not here + +- Narrative / tutorials / best practices → wiki (generated or hand-written at release time). +- Internal implementation notes → CLAUDE.md and code comments. +- Ephemeral release notes → CHANGELOG. diff --git a/schemas/help/_schema.json b/schemas/help/_schema.json new file mode 100644 index 000000000..cc568f75b --- /dev/null +++ b/schemas/help/_schema.json @@ -0,0 +1,200 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "officecli/help-schema/v1", + "title": "OfficeCLI Help Schema", + "description": "Capability schema for one (format, element) pair. Consumed by `officecli --help --json`, contract tests, and release-time wiki generation. This is the agent-facing source of truth for what officecli can do; it is updated in the same PR as the implementation.", + "type": "object", + "required": ["format", "element", "operations", "properties"], + "properties": { + "$schema": { "type": "string", "description": "pointer to this meta file for IDE tooling; ignored at runtime" }, + "format": { + "type": "string", + "enum": ["docx", "xlsx", "pptx"] + }, + "element": { + "type": "string", + "description": "element name as used in CLI, e.g. shape, paragraph, cell, chart-series" + }, + "parent": { + "description": "if this element only exists as a child of another, name the parent(s). Omit for top-level elements.", + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "note": { + "type": "string", + "description": "free-form clarification that does not fit into structured fields (e.g. schema source-of-truth pointer, invariants, caveats)" + }, + "container": { + "type": "boolean", + "description": "set to true for read-only root/container entities (presentation, workbook, document, theme, slidemaster, etc.) that are navigated through but never created or mutated. Contract tests skip Add/Set assertions for containers." + }, + "operations": { + "type": "object", + "description": "which top-level operations accept this element", + "properties": { + "add": { "type": "boolean" }, + "set": { "type": "boolean" }, + "get": { "type": "boolean" }, + "query": { "type": "boolean" }, + "remove": { "type": "boolean" } + }, + "additionalProperties": false + }, + "addParent": { + "description": "concrete CLI parent path(s) accepted by Add. Used by help renderer to print accurate `officecli add --type ` usage. Required when the element's positional/stable paths describe the element's own addressable location (e.g. /comments/comment[N], /footnotes/footnote[N]) rather than its Add-time parent. If omitted, the renderer derives parent by dropping the last segment of paths.positional[0].", + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "paths": { + "type": "object", + "description": "path forms accepted when this element is addressed by index or @id", + "properties": { + "stable": { "type": "array", "items": { "type": "string" } }, + "positional": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": false + }, + "addressing": { + "type": "object", + "description": "used when children of this element are addressed by a key attribute (e.g. axis[@role=value]) rather than [N]. Mutually exclusive with plain positional paths.", + "required": ["key", "pathForm"], + "properties": { + "key": { "type": "string", "description": "name of the keying attribute, e.g. 'role'" }, + "pathForm": { "type": "string", "description": "concrete path template, e.g. '/slide[N]/chart[N]/axis[@role=ROLE]'" }, + "keyValues": { "type": "array", "items": { "type": "string" }, "description": "permitted values for the key attribute" } + }, + "additionalProperties": false + }, + "properties": { + "type": "object", + "description": "properties supported on this element. Key = canonical property name.", + "additionalProperties": { "$ref": "#/$defs/property" } + }, + "children": { + "type": "array", + "description": "declared child element types addressable under this element in CLI paths", + "items": { "$ref": "#/$defs/childRef" } + }, + "parts": { + "type": "array", + "description": "for the synthetic `raw` element only: enumerates the raw OOXML parts addressable via the `raw` and `raw-set` commands (e.g. /workbook, /Sheet1, /styles). Rendered as a 'Parts' section by the help renderer. Not used by other elements.", + "items": { + "type": "object", + "required": ["name", "desc"], + "properties": { + "name": { "type": "string", "description": "part path as accepted by `officecli raw ` (e.g. /workbook, /Sheet1)" }, + "desc": { "type": "string", "description": "one-line description of what this part contains" } + }, + "additionalProperties": false + } + }, + "examples": { + "type": "array", + "description": "element-level command examples (in addition to per-property examples). Used primarily by the synthetic `raw` element.", + "items": { "type": "string" } + }, + "description": { + "type": "string", + "description": "element-level description (in addition to per-property descriptions). Used primarily by the synthetic `raw` element." + }, + "elementAliases": { + "type": "array", + "description": "alternate element names that should resolve to this schema (e.g. `paragraph.json` declares ['p'] so that `help docx p` works the same as `help docx paragraph`). Lets path-form abbreviations used in /body/p[N], /Sheet1/col[B], etc. line up with the help index.", + "items": { "type": "string" } + } + }, + "additionalProperties": false, + + "$defs": { + "appliesWhen": { + "type": "object", + "description": "conditional applicability. Keys are dotted paths into sibling/ancestor state (e.g. 'chartType', 'role', 'parent.chartType'). Values are arrays of admissible values. ALL keys must match (AND).", + "additionalProperties": { + "type": "array", + "items": { "type": "string" } + } + }, + "aliases": { + "description": "legacy/lenient names accepted on input, normalized to the canonical key on output. Array form = plain alias list. Object form = alias → canonical mapping (useful when one canonical has multiple aliases pointing to distinct canonical values, e.g. chartType).", + "oneOf": [ + { "type": "array", "items": { "type": "string" } }, + { "type": "object", "additionalProperties": { "type": "string" } } + ] + }, + "property": { + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "enum": ["string", "bool", "number", "color", "length", "font-size", "enum"] + }, + "description": { "type": "string" }, + "aliases": { "$ref": "#/$defs/aliases" }, + "values": { + "type": "array", + "items": { "type": "string" }, + "description": "for type=enum, the allowed canonical values" + }, + "modifiers": { + "type": "object", + "description": "for enum-like properties with orthogonal modifiers (e.g. chartType + 3d/stacked). Each modifier declares how it composes into the final value and which base values it applies to.", + "additionalProperties": { + "type": "object", + "properties": { + "prefix": { "type": "string", "description": "modifier composed as '', e.g. stackedBar" }, + "suffix": { "type": "string", "description": "modifier composed as '', e.g. column3d" }, + "example": { "type": "string" }, + "appliesWhen": { "$ref": "#/$defs/appliesWhen" } + }, + "additionalProperties": false + } + }, + "appliesWhen": { "$ref": "#/$defs/appliesWhen" }, + "requires": { + "type": "array", + "items": { "type": "string" }, + "description": "other properties on the same element that must be set together with this one for the OOXML result to be well-formed (e.g. opacity requires fill to attach alpha to). Contract tests automatically bundle `requires` entries." + }, + "add": { "type": "boolean" }, + "set": { "type": "boolean" }, + "get": { "type": "boolean" }, + "examples": { "type": "array", "items": { "type": "string" } }, + "readback": { + "type": "string", + "description": "expected format of the Get readback value" + }, + "enforcement": { + "type": "string", + "enum": ["strict", "report"], + "description": "strict = contract test failure breaks CI; report = drift only logged. New properties default to strict; historical debt may start as report and migrate." + } + }, + "additionalProperties": false + }, + "childRef": { + "type": "object", + "required": ["element", "pathSegment", "cardinality"], + "properties": { + "element": { "type": "string", "description": "name of the child element's schema file (without .json)" }, + "pathSegment": { "type": "string", "description": "CLI path segment for this child, e.g. 'series', 'title', 'axis'" }, + "cardinality": { + "type": "string", + "enum": ["0..1", "1", "0..n", "1..n"], + "description": "0..1 = singleton optional (no [N]); 1 = singleton required; 0..n / 1..n = indexed or keyed" + }, + "key": { + "type": "string", + "description": "if children are addressed by attribute instead of [N], name of that attribute (e.g. 'role' for axis[@role=value])" + }, + "keyValues": { "type": "array", "items": { "type": "string" }, "description": "permitted values for the key attribute" }, + "appliesWhen": { "$ref": "#/$defs/appliesWhen" } + }, + "additionalProperties": false + } + } +} diff --git a/schemas/help/_shared/chart-axis.json b/schemas/help/_shared/chart-axis.json new file mode 100644 index 000000000..831c0b9d7 --- /dev/null +++ b/schemas/help/_shared/chart-axis.json @@ -0,0 +1,191 @@ +{ + "$schema": "../_schema.json", + "element": "chart-axis", + "shared_base": true, + "properties": { + "axisFont": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "axis text font readback.", + "readback": "font name string", + "enforcement": "report" + }, + "axisMax": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "value-axis maximum readback (also surfaced via max on axis-by-role path).", + "readback": "numeric value", + "enforcement": "report" + }, + "axisMin": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "value-axis minimum readback (also surfaced via min on axis-by-role path).", + "readback": "numeric value", + "enforcement": "report" + }, + "axisNumFmt": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "axis number format string.", + "readback": "format code", + "enforcement": "report" + }, + "axisOrientation": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "axis scaling orientation (e.g. maxMin when reversed).", + "readback": "orientation token", + "enforcement": "report" + }, + "axisTitle": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "value-axis title readback (chart-level convenience; axis-by-role uses 'title').", + "readback": "title string", + "enforcement": "report" + }, + "format": { + "type": "string", + "description": "number format string", + "set": true, + "get": true, + "examples": [ + "--prop format=\"#,##0\"", + "--prop format=\"#,##0.00\"" + ], + "enforcement": "report" + }, + "labelOffset": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "category axis label offset (% of default 100).", + "readback": "integer percentage", + "enforcement": "report" + }, + "labelRotation": { + "type": "number", + "description": "tick label rotation in degrees", + "set": true, + "get": true, + "examples": [ + "--prop labelRotation=-45" + ], + "enforcement": "report" + }, + "logBase": { + "type": "number", + "description": "logarithmic base for value axis scale. Only valid for role=value or role=value2; ignored on category axes.", + "set": true, + "get": true, + "appliesWhen": { + "role": [ + "value", + "value2" + ] + }, + "examples": [ + "--prop logBase=10" + ], + "readback": "number (e.g. 10)", + "enforcement": "report" + }, + "majorGridlines": { + "type": "bool", + "description": "show or hide major gridlines. Applies to all roles.", + "set": true, + "get": true, + "examples": [ + "--prop majorGridlines=true" + ], + "enforcement": "report" + }, + "max": { + "type": "number", + "description": "maximum scale of the value axis. Only valid for role=value or role=value2; ignored on category axes.", + "set": true, + "get": true, + "appliesWhen": { + "role": [ + "value", + "value2" + ] + }, + "examples": [ + "--prop max=1000", + "--prop max=250" + ], + "enforcement": "report" + }, + "min": { + "type": "number", + "description": "minimum scale of the value axis. Only valid for role=value or role=value2; ignored on category axes.", + "set": true, + "get": true, + "appliesWhen": { + "role": [ + "value", + "value2" + ] + }, + "examples": [ + "--prop min=0" + ], + "enforcement": "report" + }, + "minorGridlines": { + "type": "bool", + "description": "show or hide minor gridlines. Applies to all roles.", + "set": true, + "get": true, + "examples": [ + "--prop minorGridlines=false" + ], + "enforcement": "report" + }, + "tickLabelSkip": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "category axis label skip interval (>1 means tick labels are sparser).", + "readback": "integer interval", + "enforcement": "report" + }, + "title": { + "type": "string", + "description": "axis title text. Applies to all roles (category, value). Pass 'none' to remove.", + "set": true, + "get": true, + "examples": [ + "--prop title=\"Revenue\"", + "--prop title=\"Quarter\"" + ], + "enforcement": "report" + }, + "visible": { + "type": "bool", + "description": "show or hide the axis. Applies to all roles.", + "set": true, + "get": true, + "examples": [ + "--prop visible=false" + ], + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/chart-axis.pptx-xlsx.json b/schemas/help/_shared/chart-axis.pptx-xlsx.json new file mode 100644 index 000000000..2344e7efa --- /dev/null +++ b/schemas/help/_shared/chart-axis.pptx-xlsx.json @@ -0,0 +1,53 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "dispUnits": { + "type": "enum", + "values": [ + "hundreds", + "thousands", + "tenThousands", + "hundredThousands", + "millions", + "tenMillions", + "hundredMillions", + "billions", + "trillions" + ], + "add": false, + "set": true, + "get": true, + "description": "display units for value axis labels. Applies to role=value|value2.", + "readback": "display unit token", + "examples": [ + "--prop dispUnits=thousands" + ], + "enforcement": "report" + }, + "majorUnit": { + "type": "number", + "add": false, + "set": true, + "get": true, + "description": "major tick interval on the value axis. Applies to role=value|value2.", + "readback": "numeric interval", + "examples": [ + "--prop majorUnit=20" + ], + "enforcement": "report" + }, + "minorUnit": { + "type": "number", + "add": false, + "set": true, + "get": true, + "description": "minor tick interval on the value axis. Applies to role=value|value2.", + "readback": "numeric interval", + "examples": [ + "--prop minorUnit=5" + ], + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/chart-series.json b/schemas/help/_shared/chart-series.json new file mode 100644 index 000000000..536c70ea3 --- /dev/null +++ b/schemas/help/_shared/chart-series.json @@ -0,0 +1,230 @@ +{ + "$schema": "../_schema.json", + "element": "chart-series", + "shared_base": true, + "properties": { + "categories": { + "type": "string", + "description": "per-series category override; range reference only.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop series1.categories=\"Sheet1!$A$2:$A$5\"" + ], + "enforcement": "report", + "readback": "as emitted by handler (per-format details vary)" + }, + "categoriesRef": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "A1 cell range backing the category labels.", + "readback": "A1 range string", + "enforcement": "report" + }, + "color": { + "type": "color", + "description": "series fill color.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop series1.color=#4472C4", + "--prop series1.color=4472C4" + ], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "dataLabels.numFmt": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "per-series data label number format readback.", + "readback": "format code", + "enforcement": "report" + }, + "dataLabels.separator": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "per-series data label separator string readback.", + "readback": "separator string", + "enforcement": "report" + }, + "errBars": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "error bar value type (fixed, percent, stddev, stderr); accepted aliases: fixedValue, percentage/pct, standardDeviation, standardError. Pass 'none' to clear. Silently ignored on series types that don't support error bars.", + "readback": "OOXML errValType token", + "enforcement": "lenient" + }, + "invertIfNeg": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "invert color for negative values. Bar/column/area/pie series only; line/scatter silently ignore.", + "readback": "true | false", + "enforcement": "lenient" + }, + "lineDash": { + "type": "enum", + "values": [ + "solid", + "sysDash", + "sysDot", + "sysDashDot", + "lgDash", + "lgDashDot", + "lgDashDotDot", + "dash", + "dashDot", + "dot", + "longDash" + ], + "set": true, + "get": true, + "aliases": [ + "dash" + ], + "description": "series line dash style. Set accepts user-friendly aliases (dash/dot/dashDot/longDash); Get returns OOXML token (sysDash/sysDot/sysDashDot/lgDash). 'dashDotDot' has no native enum member and is accepted as an alias for sysDashDotDot — Get readback will return sysDashDotDot. 'solid' is the only round-trip-stable value.", + "examples": [ + "--prop lineDash=dash", + "--prop lineDash=solid" + ], + "readback": "OOXML preset dash token", + "enforcement": "report" + }, + "lineWidth": { + "type": "number", + "set": true, + "get": true, + "description": "series line width in points (e.g. 1.5).", + "examples": [ + "--prop lineWidth=1.5" + ], + "readback": "numeric width in points", + "enforcement": "report" + }, + "marker": { + "type": "string", + "set": true, + "get": true, + "description": "per-series marker symbol. Values: circle, dash, diamond, dot, plus, square, star, triangle, x, none, auto. Supports 'symbol:size:COLOR' compound form (e.g. 'circle:8:FF0000'). Applies to line/scatter/radar series. (picture markers are not implemented.)", + "examples": [ + "--prop marker=circle", + "--prop marker=\"circle:8:FF0000\"" + ], + "readback": "marker symbol name", + "enforcement": "report" + }, + "markerSize": { + "type": "number", + "set": true, + "get": true, + "description": "marker size in points (2–72). Applies when marker is not 'none'.", + "examples": [ + "--prop markerSize=8" + ], + "readback": "integer", + "enforcement": "report" + }, + "markerColor": { + "type": "string", + "aliases": [ + "marker.color" + ], + "set": true, + "get": true, + "description": "per-series marker fill color (line/scatter/radar). Preserves existing symbol and size — pair with marker=/markerSize= to set the triplet independently. Round-trips via Reader's spPr/solidFill probe.", + "examples": [ + "--prop markerColor=FF0000" + ], + "readback": "hex color, e.g. #FF0000", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "series name shown in legend and data labels.", + "aliases": [ + "title" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop name=\"Q1\"", + "--prop series1.name=\"Q1\"", + "--prop name=\"Product A\"", + "--prop series1.name=\"Product A\"", + "--prop name=\"Revenue\"", + "--prop series1.name=\"Revenue\"" + ], + "readback": "series name string", + "enforcement": "report" + }, + "nameRef": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "A1 cell reference backing the series name.", + "readback": "A1 cell reference", + "enforcement": "report" + }, + "scatterStyle": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "scatter sub-style (line/lineMarker/marker/smooth/smoothMarker/none).", + "readback": "OOXML scatterStyle token", + "enforcement": "report" + }, + "secondaryAxis": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "true when the chart has more than one value axis (this series uses the secondary).", + "readback": "true | false", + "enforcement": "report" + }, + "smooth": { + "type": "string", + "description": "smooth line interpolation for line/scatter series (true|false).", + "appliesWhen": { + "parent.chartType": [ + "line", + "scatter" + ] + }, + "set": true, + "get": true, + "examples": [ + "--prop smooth=true" + ], + "readback": "true | false", + "enforcement": "report" + }, + "values": { + "type": "string", + "description": "comma-separated numbers, OR a cell range reference (Sheet1!B2:B13)", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop series1.values=\"120,150,180\"", + "--prop series1.values=\"Sheet1!$B$2:$B$5\"", + "--prop series1.values=\"120,150,180,210\"" + ], + "enforcement": "strict" + } + } +} diff --git a/schemas/help/_shared/chart-series.pptx-xlsx.json b/schemas/help/_shared/chart-series.pptx-xlsx.json new file mode 100644 index 000000000..39a1f6a46 --- /dev/null +++ b/schemas/help/_shared/chart-series.pptx-xlsx.json @@ -0,0 +1,24 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "alpha": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "series fill alpha readback in OOXML units (0..100000 = 0..100%). Distinct from chart-level `transparency` which is the percent input on Add/Set.", + "readback": "integer 0..100000 (OOXML alpha units)", + "enforcement": "report" + }, + "outlineColor": { + "type": "color", + "add": false, + "set": false, + "get": true, + "description": "per-series outline color readback.", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/chart.docx-pptx.json b/schemas/help/_shared/chart.docx-pptx.json new file mode 100644 index 000000000..3905695af --- /dev/null +++ b/schemas/help/_shared/chart.docx-pptx.json @@ -0,0 +1,44 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "radarstyle": { + "type": "string", + "appliesWhen": { + "chartType": [ + "radar" + ] + }, + "add": true, + "set": true, + "get": false, + "description": "radar chart subtype. Values: standard|line, marker, filled|fill.", + "examples": [ + "--prop radarstyle=filled" + ] + }, + "roundedcorners": { + "type": "bool", + "add": true, + "set": true, + "get": false, + "description": "round the chart-area outer corners.", + "examples": [ + "--prop roundedcorners=true" + ] + }, + "valaxisvisible": { + "type": "bool", + "aliases": [ + "valaxis.visible" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] visible (on role=value); see chart-axis schema for full axis-level options", + "examples": [ + "--prop valaxisvisible=false" + ] + } + } +} diff --git a/schemas/help/_shared/chart.docx-xlsx.json b/schemas/help/_shared/chart.docx-xlsx.json new file mode 100644 index 000000000..67f750991 --- /dev/null +++ b/schemas/help/_shared/chart.docx-xlsx.json @@ -0,0 +1,15 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "seriesCount": { + "type": "number", + "description": "number of data series in the chart (extended cx:chart only).", + "add": false, + "set": false, + "get": true, + "readback": "number of data series", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/chart.json b/schemas/help/_shared/chart.json new file mode 100644 index 000000000..63935ba4a --- /dev/null +++ b/schemas/help/_shared/chart.json @@ -0,0 +1,1463 @@ +{ + "$schema": "../_schema.json", + "element": "chart", + "shared_base": true, + "properties": { + "areafill": { + "type": "string", + "aliases": [ + "area.fill" + ], + "add": true, + "set": true, + "get": false, + "description": "fill applied to every series shape. Solid color or gradient 'c1-c2[:angle]'.", + "examples": [ + "--prop areafill=4472C4-A5C8FF:90" + ] + }, + "autotitledeleted": { + "type": "bool", + "add": true, + "set": true, + "get": false, + "description": "suppress the auto-generated 'Chart Title' placeholder.", + "examples": [ + "--prop autotitledeleted=true" + ] + }, + "axisfont": { + "type": "string", + "aliases": [ + "axis.font" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] axisFont; see chart-axis schema for full axis-level options", + "examples": [ + "--prop axisfont=10:8B949E:Helvetica" + ] + }, + "axisline": { + "type": "string", + "aliases": [ + "axis.line" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] lineWidth/lineDash; see chart-axis schema for full axis-level options", + "examples": [ + "--prop axisline=666666:1" + ] + }, + "axismax": { + "type": "number", + "aliases": [ + "max" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] max (on value/value2); see chart-axis schema for full axis-level options", + "examples": [ + "--prop axismax=1000", + "--prop axismax=250" + ] + }, + "axismin": { + "type": "number", + "aliases": [ + "min" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] min (on value/value2); see chart-axis schema for full axis-level options", + "examples": [ + "--prop axismin=0" + ] + }, + "axisnumfmt": { + "type": "string", + "aliases": [ + "axisnumberformat" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] axisNumFmt / format; see chart-axis schema for full axis-level options", + "examples": [ + "--prop axisnumfmt=\"#,##0\"" + ] + }, + "axisorientation": { + "type": "string", + "aliases": [ + "axisreverse" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] axisOrientation; see chart-axis schema for full axis-level options", + "examples": [ + "--prop axisorientation=true" + ] + }, + "axisposition": { + "type": "string", + "aliases": [ + "axispos" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] tickLabelPos / crossBetween; see chart-axis schema for full axis-level options", + "examples": [ + "--prop axisposition=top" + ] + }, + "axistitle": { + "type": "string", + "aliases": [ + "vtitle" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] title (value-axis); see chart-axis schema for full axis-level options", + "examples": [ + "--prop axistitle=\"Revenue\"" + ] + }, + "axisvisible": { + "type": "bool", + "aliases": [ + "axis.delete", + "axis.visible" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] visible; see chart-axis schema for full axis-level options", + "examples": [ + "--prop axisvisible=false" + ] + }, + "bubbleScale": { + "type": "number", + "add": true, + "set": true, + "get": true, + "description": "bubble chart scale (% of default).", + "readback": "integer percentage", + "enforcement": "report", + "aliases": [ + "bubblescale" + ], + "examples": [ + "--prop bubblescale=100" + ], + "appliesWhen": { + "chartType": [ + "bubble" + ] + } + }, + "catAxisVisible": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "description": "convenience shortcut for /chart[N]/axis[@role=...] visible (on role=category); see chart-axis schema for full axis-level options", + "readback": "true | false", + "enforcement": "report", + "aliases": [ + "cataxis.visible", + "cataxisvisible" + ], + "examples": [ + "--prop cataxisvisible=false" + ] + }, + "catTitle": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "category axis title text.", + "readback": "title string", + "enforcement": "report", + "aliases": [ + "htitle", + "cattitle" + ], + "examples": [ + "--prop cattitle=\"Quarter\"" + ] + }, + "cataxisline": { + "type": "string", + "aliases": [ + "cataxis.line" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] lineWidth/lineDash (on role=category); see chart-axis schema for full axis-level options", + "examples": [ + "--prop cataxisline=333333:1" + ] + }, + "categories": { + "type": "string", + "description": "comma-separated category labels, OR a cell range reference (e.g. Sheet1!A2:A5)", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop categories=A,B,C", + "--prop categories=\"Q1,Q2,Q3,Q4\"", + "--prop categories=\"Sheet1!$A$2:$A$5\"" + ], + "readback": "comma-separated category labels", + "enforcement": "strict" + }, + "chartFill": { + "type": "color", + "add": true, + "set": true, + "get": true, + "description": "chart-level fill color (accepts #RRGGBB, named colors, or scheme names).", + "readback": "#RRGGBB or color descriptor", + "enforcement": "report" + }, + "chartType": { + "type": "enum", + "values": [ + "bar", + "column", + "line", + "pie", + "doughnut", + "area", + "scatter", + "bubble", + "radar", + "stock", + "combo", + "waterfall", + "funnel", + "treemap", + "sunburst", + "boxWhisker", + "histogram", + "pareto" + ], + "modifiers": { + "3d": { + "suffix": "3d", + "example": "column3d", + "appliesWhen": { + "chartType": [ + "bar", + "column", + "line", + "pie", + "area" + ] + } + }, + "stacked": { + "prefix": "stacked", + "example": "stackedBar", + "appliesWhen": { + "chartType": [ + "bar", + "column", + "line", + "area" + ] + } + }, + "percentStacked": { + "prefix": "percentStacked", + "example": "percentStackedBar", + "appliesWhen": { + "chartType": [ + "bar", + "column", + "line", + "area" + ] + } + } + }, + "aliases": [ + "type", + "col", + "donut", + "xy", + "spider", + "ohlc", + "wf", + "charttype" + ], + "propAliases": [ + "type" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop chartType=column", + "--prop chartType=stackedBar", + "--prop chartType=percentStackedColumn", + "--prop chartType=column3d", + "--prop chartType=waterfall" + ], + "readback": "normalized chartType string without modifiers (modifiers surface as separate flags in later iterations)", + "enforcement": "strict" + }, + "chartareafill": { + "type": "string", + "aliases": [ + "chartfill" + ], + "add": true, + "set": true, + "get": false, + "description": "chart-area background fill. Solid color, gradient, or 'none'.", + "examples": [ + "--prop chartareafill=FFFFFF" + ] + }, + "chartborder": { + "type": "string", + "aliases": [ + "chartarea.border" + ], + "add": true, + "set": true, + "get": false, + "description": "chart-area outer border line. Same format as plotborder.", + "examples": [ + "--prop chartborder=000000:1", + "--prop chartborder=none" + ] + }, + "colorrule": { + "type": "string", + "aliases": [ + "conditionalcolor", + "colorRule" + ], + "add": true, + "set": true, + "get": false, + "description": "conditional per-data-point color. Format: 'threshold:belowColor:aboveColor'.", + "examples": [ + "--prop colorrule=0:FF0000:00AA00" + ] + }, + "colors": { + "type": "string", + "description": "comma-separated series fill colors, positional (1st color → series 1). Per-series dotted keys (series1.color=...) override positions.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop colors=\"4472C4,ED7D31,A5A5A5\"" + ], + "enforcement": "strict" + }, + "combosplit": { + "type": "number", + "add": true, + "set": false, + "get": false, + "description": "combo chart split index: first N series use primary chart type, rest use secondary. Add-time only.", + "examples": [ + "--prop combosplit=2" + ] + }, + "combotypes": { + "type": "string", + "aliases": [ + "combo.types" + ], + "add": true, + "set": true, + "get": false, + "description": "rebuild as combo chart with per-series chart types (column,line,area,...). Comma-separated, one per series.", + "examples": [ + "--prop combotypes=\"column,column,line\"" + ] + }, + "crossBetween": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "category axis cross-between behavior (between / midCat).", + "examples": [ + "--prop crossBetween=between", + "--prop crossbetween=midcat" + ], + "readback": "crossBetween token", + "enforcement": "report", + "aliases": [ + "crossbetween" + ] + }, + "crosses": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "where the value axis crosses the category axis. Values: autoZero (default), max, min.", + "examples": [ + "--prop crosses=max" + ], + "readback": "crosses token" + }, + "crossesAt": { + "type": "number", + "add": true, + "set": true, + "get": true, + "description": "value-axis crossesAt value readback.", + "readback": "numeric value", + "enforcement": "report", + "aliases": [ + "crossesat" + ], + "examples": [ + "--prop crossesat=0" + ] + }, + "data": { + "type": "string", + "description": "inline series spec 'Name:1,2,3' or 'Name1:1,2,3;Name2:4,5,6'. Add-time only; use per-series chart-series Set after creation.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop data=\"Sales:10,20,30\"", + "--prop data=\"Sales:10,20,30;Cost:5,8,12\"" + ], + "readback": "n/a", + "enforcement": "report" + }, + "dataLabels": { + "type": "string", + "aliases": [ + "datalabels", + "labels" + ], + "add": true, + "set": true, + "get": true, + "description": "show/hide data labels. Use 'none' to hide; otherwise comma list of flags: value, percent, category, series, all (also accepts seriesName/categoryName/percentage/values aliases). Position values (outsideEnd/center/insideEnd/insideBase/top/bottom/left/right/bestFit) implicitly enable showVal and apply as dLblPos.", + "examples": [ + "--prop dataLabels=value", + "--prop dataLabels=\"value,percent\"", + "--prop dataLabels=outsideEnd", + "--prop dataLabels=none" + ], + "readback": "comma-separated flags: value,percent,category,series" + }, + "dataRange": { + "type": "string", + "aliases": [ + "datarange", + "range" + ], + "add": true, + "set": false, + "get": false, + "description": "external workbook range source for series; Add-time only.", + "examples": [ + "--prop dataRange=Sheet1!A1:D5" + ] + }, + "dataTable": { + "type": "bool", + "aliases": [ + "datatable" + ], + "add": true, + "set": true, + "get": true, + "description": "show data table beneath the chart (with default borders + legend keys).", + "examples": [ + "--prop dataTable=true" + ], + "readback": "true | false" + }, + "decreaseColor": { + "type": "color", + "add": true, + "set": false, + "get": false, + "description": "waterfall: negative bar color. Add-time only.", + "examples": [ + "--prop decreaseColor=FF0000" + ] + }, + "dispBlanksAs": { + "type": "enum", + "values": [ + "gap", + "zero", + "span" + ], + "add": true, + "set": true, + "get": true, + "description": "how empty cells render (gap leaves a hole, zero plots as 0, span connects across).", + "examples": [ + "--prop dispBlanksAs=gap" + ], + "readback": "dispBlanksAs token", + "enforcement": "report" + }, + "droplines": { + "type": "string", + "appliesWhen": { + "chartType": [ + "line" + ] + }, + "add": true, + "set": true, + "get": false, + "description": "drop lines on line chart. true|false toggle or line spec 'color[:width[:dash]]'; 'none' removes.", + "examples": [ + "--prop droplines=true", + "--prop droplines=808080:0.5" + ] + }, + "errbars": { + "type": "string", + "aliases": [ + "errorbars" + ], + "add": true, + "set": true, + "get": false, + "description": "error bars on each series. Format: 'type:value' where type ∈ fixed (alias: fixedValue), percent (alias: percentage, pct), stddev (alias: standardDeviation), stderr (alias: standardError). 'none' removes.", + "examples": [ + "--prop errbars=fixed:5", + "--prop errbars=none", + "--prop errbars=percent:10" + ] + }, + "explosion": { + "type": "number", + "aliases": [ + "explode" + ], + "add": true, + "set": true, + "get": true, + "description": "pie/doughnut slice explosion 0..100 (percent of radius); 0 removes.", + "examples": [ + "--prop explosion=10" + ], + "readback": "as emitted by handler (per-format details vary)" + }, + "firstSliceAngle": { + "type": "number", + "add": true, + "set": true, + "get": true, + "description": "pie/doughnut first slice angle (degrees).", + "readback": "integer degrees", + "enforcement": "report", + "aliases": [ + "sliceangle", + "firstsliceangle" + ], + "examples": [ + "--prop firstsliceangle=90" + ], + "appliesWhen": { + "chartType": [ + "pie" + ] + } + }, + "gapdepth": { + "type": "number", + "appliesWhen": { + "chartType": [ + "bar3d", + "line3d", + "area3d" + ] + }, + "add": true, + "set": true, + "get": false, + "description": "depth gap between series in 3D bar/line/area charts (percent).", + "examples": [ + "--prop gapdepth=150" + ] + }, + "gapwidth": { + "type": "number", + "aliases": [ + "gap" + ], + "add": true, + "set": true, + "get": true, + "description": "gap between bar/column groups, 0..500 (percent of bar width).", + "examples": [ + "--prop gapwidth=150" + ], + "readback": "integer 0..500" + }, + "gradient": { + "type": "string", + "aliases": [ + "gradientfill" + ], + "add": true, + "set": true, + "get": true, + "description": "gradient fill applied to every series. Format: 'c1-c2[-c3][:angle]' (angle in degrees). Errors if chart has no series.", + "examples": [ + "--prop gradient=FF0000-0000FF", + "--prop gradient=FF0000-00FF00-0000FF:90" + ], + "readback": "as emitted by handler (per-format details vary)" + }, + "gradients": { + "type": "string", + "add": true, + "set": true, + "get": false, + "description": "per-series gradient fills, semicolon-separated; one entry per series.", + "examples": [ + "--prop gradients=\"FF0000-0000FF;00FF00-FFFF00\"" + ] + }, + "gridlines": { + "type": "bool", + "aliases": [ + "majorgridlines" + ], + "add": true, + "set": true, + "get": true, + "description": "value-axis major gridlines. true|false toggle, or line spec 'color', 'color:width', 'color:width:dash' to style; 'none' removes.", + "examples": [ + "--prop gridlines=true", + "--prop gridlines=E0E0E0:0.3", + "--prop gridlines=none" + ], + "readback": "true | false" + }, + "height": { + "type": "length", + "add": true, + "set": true, + "get": true, + "description": "chart frame height; accepts cm/in/pt/EMU. Ignored if anchor= is set.", + "examples": [ + "--prop height=10cm" + ] + }, + "hilowlines": { + "type": "string", + "appliesWhen": { + "chartType": [ + "line", + "stock" + ] + }, + "add": true, + "set": true, + "get": false, + "description": "high-low lines on line/stock chart. Same format as droplines.", + "examples": [ + "--prop hilowlines=true" + ] + }, + "holeSize": { + "type": "number", + "add": true, + "set": true, + "get": true, + "description": "doughnut hole size readback.", + "readback": "integer 10..90 percent", + "enforcement": "report", + "aliases": [ + "holesize" + ], + "examples": [ + "--prop holesize=50", + "--prop holeSize=50" + ], + "appliesWhen": { + "chartType": [ + "doughnut" + ] + } + }, + "increaseColor": { + "type": "color", + "add": true, + "set": false, + "get": false, + "description": "waterfall: positive bar color. Add-time only.", + "examples": [ + "--prop increaseColor=00AA00" + ] + }, + "invertifneg": { + "type": "bool", + "aliases": [ + "invertifnegative" + ], + "add": true, + "set": true, + "get": false, + "description": "if true, draw negative bars in an inverted (lighter) color.", + "examples": [ + "--prop invertifneg=true" + ] + }, + "labelPos": { + "type": "string", + "aliases": [ + "labelpos", + "labelposition" + ], + "add": true, + "set": true, + "get": true, + "description": "data label position. Values: center|ctr, insideEnd|inEnd|inside, insideBase|inBase|base, outsideEnd|outEnd|outside, bestFit|best|auto, top|t, bottom|b, left|l, right|r. Restrictions: not supported on doughnut/area/radar/stock; pie/pie3D restricted to ctr/inEnd/inBase/bestFit (other tokens rejected, per OOXML ST_DLblPosPie); stacked series clamp to ctr/inBase/inEnd; combo charts skip entirely.", + "examples": [ + "--prop labelPos=outsideEnd" + ], + "readback": "OOXML position token: ctr/inEnd/inBase/outEnd/bestFit/t/b/l/r" + }, + "labelfont": { + "type": "string", + "add": true, + "set": true, + "get": false, + "description": "data label text font. Format: 'size:color:fontname' (any segment optional). Readback is split into labelFont.size / labelFont.color / labelFont.bold / labelFont.name (one key per slot) so dump→replay can rebuild the compound spec without parsing.", + "examples": [ + "--prop labelfont=9:333333:Calibri" + ] + }, + "labelFont.size": { + "type": "number", + "get": true, + "description": "data label font size in points. Readback of the size slot inside labelfont= (BuildLabelTextProperties' DefaultRunProperties.FontSize).", + "examples": [] + }, + "labelFont.color": { + "type": "string", + "get": true, + "description": "data label font color (hex, e.g. #FF0000). Readback of the color slot inside labelfont=.", + "examples": [] + }, + "labelFont.bold": { + "type": "bool", + "get": true, + "description": "data label font bold flag. Emitted only when true.", + "examples": [] + }, + "labelFont.name": { + "type": "string", + "get": true, + "description": "data label font typeface (latin). Readback of the fontname slot inside labelfont=.", + "examples": [] + }, + "labeloffset": { + "type": "number", + "add": true, + "set": true, + "get": false, + "description": "category-axis label offset 0..1000 (percent of font height); category axis only.", + "examples": [ + "--prop labeloffset=100" + ] + }, + "labelrotation": { + "type": "number", + "aliases": [ + "xaxis.labelrotation", + "valaxis.labelrotation", + "yaxis.labelrotation", + "xaxis.labelRotation", + "valaxis.labelRotation", + "yaxis.labelRotation", + "xaxislabelrotation", + "valaxislabelrotation", + "yaxislabelrotation" + ], + "add": true, + "set": true, + "get": false, + "description": "tick-label rotation in degrees (-90..90). Bare 'labelrotation' targets both axes; xaxis.* targets category, yaxis./valaxis.* targets value.", + "examples": [ + "--prop labelrotation=-45", + "--prop xaxis.labelrotation=30" + ] + }, + "leaderlines": { + "type": "bool", + "aliases": [ + "showleaderlines" + ], + "add": true, + "set": true, + "get": false, + "description": "show/hide leader lines connecting data labels to slices (pie/doughnut).", + "examples": [ + "--prop leaderlines=true" + ] + }, + "legend": { + "type": "enum", + "values": [ + "true", + "false", + "none", + "top", + "bottom", + "left", + "right", + "topRight", + "tr" + ], + "add": true, + "set": true, + "get": true, + "description": "legend position. 'none'/'false' hides; otherwise place at top|t, bottom|b, left|l, right|r, topRight|tr. Hyphen and underscore variants accepted.", + "examples": [ + "--prop legend=bottom", + "--prop legend=none" + ] + }, + "legend.overlay": { + "type": "bool", + "aliases": [ + "legendoverlay" + ], + "add": true, + "set": true, + "get": true, + "description": "if true, legend overlays the plot area instead of reserving space.", + "examples": [ + "--prop legend.overlay=true" + ], + "readback": "true | false" + }, + "legendFont": { + "type": "string", + "aliases": [ + "legendfont", + "legend.font" + ], + "add": true, + "set": true, + "get": true, + "description": "legend text font. Format: 'size:color:fontname' (any segment optional).", + "examples": [ + "--prop legendFont=10:CCCCCC:Arial", + "--prop legendFont=9:808080" + ], + "readback": "size:color:fontname" + }, + "linedash": { + "type": "string", + "aliases": [ + "dash" + ], + "add": true, + "set": true, + "get": false, + "description": "line dash style for every series. Values: solid, dash, dashDot, dot, lgDash, lgDashDot, sysDash, sysDot, sysDashDot.", + "examples": [ + "--prop linedash=dash" + ] + }, + "linewidth": { + "type": "number", + "add": true, + "set": true, + "get": false, + "description": "line width in points (applies to every series line).", + "examples": [ + "--prop linewidth=2" + ] + }, + "logbase": { + "type": "number", + "aliases": [ + "logscale", + "yaxisscale" + ], + "add": true, + "set": true, + "get": false, + "description": "value-axis logarithmic base (2..1000 typically). Shorthand: true|yes|log|1 → base 10; false|none|linear|0 removes log scale.", + "examples": [ + "--prop logbase=10", + "--prop logscale=true", + "--prop yaxisscale=linear" + ] + }, + "majorTickMark": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "major tick mark style (out / in / cross / none).", + "examples": [ + "--prop majorTickMark=out", + "--prop majortickmark=out" + ], + "readback": "tick mark token", + "enforcement": "report", + "aliases": [ + "majortick", + "majortickmark" + ] + }, + "majorunit": { + "type": "number", + "add": true, + "set": true, + "get": false, + "description": "value-axis major gridline / tick spacing.", + "examples": [ + "--prop majorunit=200", + "--prop majorunit=50" + ] + }, + "marker": { + "type": "string", + "aliases": [ + "markers" + ], + "add": true, + "set": true, + "get": false, + "description": "marker symbol for line/scatter/radar series only (other types silently skipped). Format: 'symbol' or 'symbol:size' or 'symbol:size:color'. Symbols: none, auto, circle, square, diamond, triangle, x, plus, star, dash, dot. (picture markers are not implemented.) Chart-level Get does not surface marker because applicability is chart-type-conditional — read per-series via /chart[N]/series[K] (chart-series schema declares marker get:true).", + "examples": [ + "--prop marker=circle", + "--prop marker=square:8:FF0000" + ], + "readback": "as emitted by handler (per-format details vary)" + }, + "markersize": { + "type": "number", + "add": true, + "set": true, + "get": false, + "description": "marker size 2..72 (line/scatter/radar series only).", + "examples": [ + "--prop markersize=8" + ] + }, + "markercolor": { + "type": "string", + "aliases": [ + "marker.color" + ], + "set": true, + "get": true, + "description": "marker fill color (line/scatter/radar series only). Fans out to every applicable series; preserves existing marker symbol/size. Read back from the first series' marker spPr/solidFill.", + "examples": [ + "--prop markerColor=FF0000" + ], + "readback": "as emitted by handler (hex color, e.g. #FF0000)" + }, + "minorGridlines": { + "type": "bool", + "aliases": [ + "minorgridlines" + ], + "add": true, + "set": true, + "get": true, + "description": "value-axis minor gridlines; same format as gridlines.", + "examples": [ + "--prop minorGridlines=true", + "--prop minorGridlines=F0F0F0:0.25" + ], + "readback": "true | false" + }, + "minorTickMark": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "minor tick mark style (out / in / cross / none).", + "examples": [ + "--prop minorTickMark=none", + "--prop minortickmark=in" + ], + "readback": "tick mark token", + "enforcement": "report", + "aliases": [ + "minortick", + "minortickmark" + ] + }, + "minorunit": { + "type": "number", + "add": true, + "set": true, + "get": false, + "description": "value-axis minor gridline / tick spacing.", + "examples": [ + "--prop minorunit=50", + "--prop minorunit=10" + ] + }, + "overlap": { + "type": "number", + "add": true, + "set": true, + "get": true, + "description": "bar/column overlap within a group, -100..100 (negative = gap, positive = overlap).", + "examples": [ + "--prop overlap=0", + "--prop overlap=100" + ], + "readback": "as emitted by handler (per-format details vary)" + }, + "plotFill": { + "type": "color", + "aliases": [ + "plotareafill", + "plotfill" + ], + "add": true, + "set": true, + "get": true, + "description": "plot-area background fill. Solid color, gradient 'c1-c2[:angle]', or 'none'.", + "examples": [ + "--prop plotFill=FAFAFA", + "--prop plotareafill=FAFAFA", + "--prop plotFill=none" + ], + "readback": "#RRGGBB or color descriptor" + }, + "plotborder": { + "type": "string", + "aliases": [ + "plotarea.border" + ], + "add": true, + "set": true, + "get": false, + "description": "plot-area border line. Format: 'color', 'color:width', 'color:width:dash'; or 'none'.", + "examples": [ + "--prop plotborder=CCCCCC:0.5", + "--prop plotborder=none" + ] + }, + "plotvisonly": { + "type": "bool", + "aliases": [ + "plotvisibleonly" + ], + "add": true, + "set": true, + "get": false, + "description": "if true, skip plotting hidden worksheet rows/columns.", + "examples": [ + "--prop plotvisonly=true" + ] + }, + "preset": { + "type": "string", + "aliases": [ + "theme", + "style.preset" + ], + "add": true, + "set": true, + "get": false, + "description": "named style bundle. Values: minimal, dark, corporate, magazine, dashboard, colorful, monochrome (alias mono).", + "examples": [ + "--prop preset=minimal", + "--prop preset=corporate", + "--prop preset=dark" + ] + }, + "referenceline": { + "type": "string", + "aliases": [ + "refline", + "targetline" + ], + "add": true, + "set": true, + "get": false, + "description": "horizontal reference / target line. Format: 'value' or 'value:color' or 'value:color:label' or 'value:color:label:dash'. Pass 'none' to remove.", + "examples": [ + "--prop referenceline=100:FF0000:Target", + "--prop referenceline=none", + "--prop refline=80:00AA00" + ] + }, + "scatterstyle": { + "type": "string", + "appliesWhen": { + "chartType": [ + "scatter" + ] + }, + "add": true, + "set": true, + "get": false, + "description": "scatter chart subtype. Values: line|lineOnly, lineMarker, marker|markerOnly, smooth|smoothLine, smoothMarker.", + "examples": [ + "--prop scatterstyle=smoothMarker" + ] + }, + "secondaryaxis": { + "type": "string", + "aliases": [ + "secondary" + ], + "add": true, + "set": true, + "get": false, + "description": "comma-separated 1-based series indices to plot on a secondary value axis.", + "examples": [ + "--prop secondaryaxis=2", + "--prop secondary=\"2,3\"" + ] + }, + "seriesoutline": { + "type": "string", + "aliases": [ + "series.outline" + ], + "add": true, + "set": true, + "get": false, + "description": "series outline. Format: 'color', 'color:width', or 'color:width:dash' (also accepts '-' separator); 'none' removes.", + "examples": [ + "--prop seriesoutline=000000:0.5", + "--prop seriesoutline=none" + ] + }, + "seriesshadow": { + "type": "string", + "aliases": [ + "series.shadow" + ], + "add": true, + "set": true, + "get": false, + "description": "outer shadow on every series shape. Format: 'COLOR-BLUR-ANGLE-DIST-OPACITY'; 'none' removes.", + "examples": [ + "--prop seriesshadow=000000-5-45-3-50", + "--prop seriesshadow=none" + ] + }, + "serlines": { + "type": "string", + "aliases": [ + "serieslines" + ], + "add": true, + "set": true, + "get": false, + "description": "series lines on stacked bar charts (true/false).", + "examples": [ + "--prop serlines=true" + ] + }, + "shape": { + "type": "string", + "aliases": [ + "barshape" + ], + "appliesWhen": { + "chartType": [ + "bar3d" + ] + }, + "add": true, + "set": true, + "get": false, + "description": "3D bar shape. Values: box|cuboid, cone, coneToMax, cylinder, pyramid, pyramidToMax. Bar3D charts only.", + "examples": [ + "--prop shape=cylinder" + ] + }, + "showMarker": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "show markers on line/scatter series at chart level (true|false).", + "examples": [ + "--prop showMarker=true" + ], + "readback": "true | false", + "enforcement": "report" + }, + "shownegbubbles": { + "type": "bool", + "appliesWhen": { + "chartType": [ + "bubble" + ] + }, + "add": true, + "set": true, + "get": false, + "description": "render negative-valued bubbles. Bubble charts only.", + "examples": [ + "--prop shownegbubbles=true" + ] + }, + "sizerepresents": { + "type": "string", + "appliesWhen": { + "chartType": [ + "bubble" + ] + }, + "add": true, + "set": true, + "get": false, + "description": "how bubble size value is mapped. Values: area (default), width|w. Bubble charts only.", + "examples": [ + "--prop sizerepresents=area" + ] + }, + "smooth": { + "type": "string", + "appliesWhen": { + "chartType": [ + "line", + "scatter" + ] + }, + "add": true, + "set": true, + "get": true, + "description": "smooth lines on line/scatter charts (true|false). Reported unsupported for other chart types.", + "examples": [ + "--prop smooth=true" + ], + "readback": "as emitted by handler (per-format details vary)" + }, + "style": { + "type": "number", + "aliases": [ + "styleid" + ], + "add": true, + "set": true, + "get": true, + "description": "built-in chart style id 1..48; pass 'none' to clear.", + "examples": [ + "--prop style=2" + ], + "readback": "as emitted by handler (per-format details vary)" + }, + "tickLabelPos": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "tick label position (high / low / nextTo / none).", + "examples": [ + "--prop tickLabelPos=nextTo", + "--prop ticklabelpos=low" + ], + "readback": "tick label position token", + "enforcement": "report", + "aliases": [ + "ticklabelposition", + "ticklabelpos" + ] + }, + "ticklabelskip": { + "type": "number", + "aliases": [ + "tickskip" + ], + "add": true, + "set": true, + "get": false, + "description": "draw tick labels every Nth category (category axis).", + "examples": [ + "--prop ticklabelskip=2" + ] + }, + "title": { + "type": "string", + "description": "chart title text; pass 'none' to remove an existing title. Get also returns sub-keys title.font, title.size, title.color, title.bold when set; these are get-only readback fields surfaced from chart title runs.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop title=\"Q1\"", + "--prop title=\"2024 Sales\"", + "--prop title=none" + ], + "readback": "chart title", + "enforcement": "report" + }, + "title.bold": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "title bold flag (true|false).", + "readback": "true | false" + }, + "title.color": { + "type": "color", + "add": true, + "set": true, + "get": true, + "description": "title font color (#RRGGBB, named, or scheme color).", + "readback": "#RRGGBB" + }, + "title.font": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "title font name.", + "readback": "font name" + }, + "title.size": { + "type": "font-size", + "add": true, + "set": true, + "get": true, + "description": "title font size (e.g. 14 or 14pt).", + "readback": "Npt" + }, + "totalColor": { + "type": "color", + "add": true, + "set": false, + "get": false, + "description": "waterfall: subtotal/total bar color. Add-time only.", + "examples": [ + "--prop totalColor=4472C4" + ] + }, + "transparency": { + "type": "number", + "aliases": [ + "opacity", + "alpha" + ], + "add": true, + "set": true, + "get": false, + "description": "series fill transparency (0..100, percent). 'transparency' is inverse of 'opacity'/'alpha' (transparency=30 ≡ opacity=70).", + "examples": [ + "--prop transparency=30", + "--prop opacity=70" + ] + }, + "trendline": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "add trendline to every series. Format: 'type[:order]' or 'type:forward:backward'. Types: linear (default), exp|exponential, log|logarithmic, poly|polynomial, power, movingAvg|moving|movingAverage. Order applies to poly/movingAvg. Pass 'none' to clear.", + "examples": [ + "--prop trendline=linear", + "--prop trendline=poly:3", + "--prop trendline=none", + "--prop trendline=movingAvg:3" + ], + "readback": "as emitted by handler (per-format details vary)" + }, + "updownbars": { + "type": "string", + "appliesWhen": { + "chartType": [ + "line", + "stock" + ] + }, + "add": true, + "set": true, + "get": false, + "description": "up/down bars on line chart. true | 'gapWidth:upColor:downColor' | 'none'/'false'.", + "examples": [ + "--prop updownbars=true", + "--prop updownbars=150:00AA00:FF0000" + ] + }, + "valaxisline": { + "type": "string", + "aliases": [ + "valaxis.line" + ], + "add": true, + "set": true, + "get": false, + "description": "convenience shortcut for /chart[N]/axis[@role=...] lineWidth/lineDash (on role=value); see chart-axis schema for full axis-level options", + "examples": [ + "--prop valaxisline=333333:1" + ] + }, + "varyColors": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "vary colors by data point (true|false; single-series charts).", + "examples": [ + "--prop varyColors=true" + ], + "readback": "true | false", + "enforcement": "report" + }, + "view3d": { + "type": "string", + "aliases": [ + "camera", + "perspective" + ], + "add": true, + "set": true, + "get": true, + "description": "3D view angles. Format: 'rotX,rotY,perspective' (any tail optional) or single integer for perspective only. Named-key form (rotX=...) is rejected.", + "examples": [ + "--prop view3d=15,20,30", + "--prop view3d=20", + "--prop perspective=30" + ], + "readback": "as emitted by handler (per-format details vary)" + }, + "width": { + "type": "length", + "add": true, + "set": true, + "get": true, + "description": "chart frame width; accepts cm/in/pt/EMU. Ignored if anchor= is set.", + "examples": [ + "--prop width=18cm", + "--prop width=15cm" + ] + } + } +} diff --git a/schemas/help/_shared/chart.pptx-xlsx.json b/schemas/help/_shared/chart.pptx-xlsx.json new file mode 100644 index 000000000..a4f99fa57 --- /dev/null +++ b/schemas/help/_shared/chart.pptx-xlsx.json @@ -0,0 +1,50 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "anchor": { + "type": "string", + "add": true, + "set": true, + "get": false, + "description": "absolute placement on slide; cm-based 'x,y,w,h' or named anchor token.", + "examples": [ + "--prop anchor=D2:J18", + "--prop anchor=2cm,3cm,18cm,10cm" + ] + }, + "dispunits": { + "type": "string", + "aliases": [ + "displayunits" + ], + "add": true, + "set": true, + "get": false, + "description": "value-axis display units divisor. Values: none, hundreds, thousands, tenThousands|10000, hundredThousands|100000, millions, tenMillions|10000000, hundredMillions|100000000, billions, trillions.", + "examples": [ + "--prop dispunits=thousands" + ] + }, + "x": { + "type": "length", + "add": true, + "set": true, + "get": true, + "description": "absolute X position from sheet origin; accepts cm/in/pt/EMU. Ignored if anchor= is set.", + "examples": [ + "--prop x=2cm" + ] + }, + "y": { + "type": "length", + "add": true, + "set": true, + "get": true, + "description": "absolute Y position from sheet origin; accepts cm/in/pt/EMU. Ignored if anchor= is set.", + "examples": [ + "--prop y=3cm" + ] + } + } +} diff --git a/schemas/help/_shared/comment.docx-pptx.json b/schemas/help/_shared/comment.docx-pptx.json new file mode 100644 index 000000000..a01dbbf9e --- /dev/null +++ b/schemas/help/_shared/comment.docx-pptx.json @@ -0,0 +1,32 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "date": { + "type": "string", + "description": "ISO-8601 timestamp. Defaults to DateTime.UtcNow.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop date=2025-01-15T10:30:00Z", + "--prop date=2025-01-15T10:00:00Z" + ], + "readback": "Date attribute", + "enforcement": "report" + }, + "initials": { + "type": "string", + "description": "author initials. Defaults to derived from author name when omitted.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop initials=AT", + "--prop initials=AW" + ], + "readback": "initials", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/comment.json b/schemas/help/_shared/comment.json new file mode 100644 index 000000000..f2812169f --- /dev/null +++ b/schemas/help/_shared/comment.json @@ -0,0 +1,32 @@ +{ + "$schema": "../_schema.json", + "element": "comment", + "shared_base": true, + "properties": { + "author": { + "type": "string", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop author=\"Alice\"" + ], + "readback": "Author attribute", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "comment body. Required.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop text=\"Check formula\"", + "--prop text=\"Reword this bullet\"", + "--prop text=\"Review this\"" + ], + "readback": "concatenated text", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/equation.json b/schemas/help/_shared/equation.json new file mode 100644 index 000000000..b4f03f3bb --- /dev/null +++ b/schemas/help/_shared/equation.json @@ -0,0 +1,22 @@ +{ + "$schema": "../_schema.json", + "element": "equation", + "shared_base": true, + "properties": { + "formula": { + "type": "string", + "description": "math expression. Aliases: text.", + "aliases": [ + "text" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop formula=\"x^2 + y^2 = z^2\"" + ], + "readback": "n/a in the shared base. pptx overrides to get:true (LaTeX reconstructed from ); docx does not surface a formula key on Get.", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/hyperlink.json b/schemas/help/_shared/hyperlink.json new file mode 100644 index 000000000..d0c659fdf --- /dev/null +++ b/schemas/help/_shared/hyperlink.json @@ -0,0 +1,6 @@ +{ + "$schema": "../_schema.json", + "element": "hyperlink", + "shared_base": true, + "properties": {} +} diff --git a/schemas/help/_shared/ole.docx-pptx.json b/schemas/help/_shared/ole.docx-pptx.json new file mode 100644 index 000000000..3de1db841 --- /dev/null +++ b/schemas/help/_shared/ole.docx-pptx.json @@ -0,0 +1,30 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "height": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop height=8cm", + "--prop height=2in" + ], + "readback": "unit-qualified length from inline style (e.g. \"5cm\")", + "enforcement": "report" + }, + "width": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop width=10cm", + "--prop width=3in" + ], + "readback": "unit-qualified length from inline style (e.g. \"5cm\")", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/ole.json b/schemas/help/_shared/ole.json new file mode 100644 index 000000000..ec2f23399 --- /dev/null +++ b/schemas/help/_shared/ole.json @@ -0,0 +1,51 @@ +{ + "$schema": "../_schema.json", + "element": "ole", + "shared_base": true, + "properties": { + "preview": { + "type": "string", + "description": "preview thumbnail image source. Add-time only — Set ignores this key.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop preview=/path/to/thumb.png" + ], + "readback": "n/a", + "enforcement": "report" + }, + "progId": { + "type": "string", + "description": "OLE ProgID (e.g. 'Excel.Sheet.12'). Usually inferred from src extension.", + "aliases": [ + "progid" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop progId=Word.Document.12", + "--prop progId=Excel.Sheet.12" + ], + "readback": "ProgID string", + "enforcement": "report" + }, + "src": { + "type": "string", + "description": "embedded object source — file path, URL, or data-URI; accepted on add/set only. Get does NOT surface this key; the embedded relationship id is exposed under a separate Format[\"relId\"] key.", + "aliases": [ + "path" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop src=/path/to/data.docx", + "--prop src=/path/to/data.xlsx" + ], + "readback": "add/set-only input; not echoed by Get. Use Format[\"relId\"] to inspect the embedded relationship.", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/ole.pptx-xlsx.json b/schemas/help/_shared/ole.pptx-xlsx.json new file mode 100644 index 000000000..a89707e83 --- /dev/null +++ b/schemas/help/_shared/ole.pptx-xlsx.json @@ -0,0 +1,33 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "contentType": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "MIME type of the embedded part.", + "readback": "MIME type string", + "enforcement": "report" + }, + "fileSize": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "embedded payload bytes.", + "readback": "integer byte count", + "enforcement": "report" + }, + "objectType": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "OLE object type marker (always 'ole').", + "readback": "literal string 'ole'", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/paragraph.json b/schemas/help/_shared/paragraph.json new file mode 100644 index 000000000..0f4a7c1e6 --- /dev/null +++ b/schemas/help/_shared/paragraph.json @@ -0,0 +1,72 @@ +{ + "$schema": "../_schema.json", + "element": "paragraph", + "shared_base": true, + "properties": { + "indent": { + "type": "length", + "description": "left indentation. Routed through SpacingConverter — accepts twips int or unit-qualified (2cm/0.5in/24pt). Aliases: leftindent/leftIndent/indentleft.", + "aliases": [ + "leftindent", + "leftIndent" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop indent=2cm" + ], + "readback": "length string (cm or twips, format-dependent)", + "enforcement": "report" + }, + "lineSpacing": { + "type": "string", + "description": "multiplier (e.g. 1.5x, 150%) or fixed length (e.g. 18pt)", + "aliases": [ + "linespacing" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lineSpacing=1.5x", + "--prop lineSpacing=18pt" + ], + "readback": "\"x\" for multiplier or \"pt\" for fixed", + "enforcement": "strict" + }, + "lineRule": { + "type": "enum", + "description": "line spacing rule paired with lineSpacing. 'auto' = multiplier (default for 1.5x/150%), 'exact' = exact fixed height (default for Npt), 'atLeast' = minimum height (Npt floor; lines may grow to fit tall content).", + "values": [ + "auto", + "exact", + "atLeast" + ], + "aliases": [ + "linerule" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lineSpacing=14pt --prop lineRule=atLeast" + ], + "readback": "auto | exact | atLeast", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "Sets plain text on the paragraph by creating an implicit single run. Do not also add a 'run' child with text on the same paragraph — they will duplicate.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop text=\"Hello\"", + "--prop text=\"Hello world\"" + ], + "readback": "plain text content of paragraph", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/_shared/picture.docx-pptx.json b/schemas/help/_shared/picture.docx-pptx.json new file mode 100644 index 000000000..33c4284cc --- /dev/null +++ b/schemas/help/_shared/picture.docx-pptx.json @@ -0,0 +1,15 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "id": { + "type": "number", + "description": "OOXML shape id; source of the @id in the stable path /picture[@id=ID].", + "add": false, + "set": false, + "get": true, + "readback": "integer shape id", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/picture.docx-xlsx.json b/schemas/help/_shared/picture.docx-xlsx.json new file mode 100644 index 000000000..c02ec0347 --- /dev/null +++ b/schemas/help/_shared/picture.docx-xlsx.json @@ -0,0 +1,18 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "fallback": { + "type": "string", + "description": "optional PNG fallback for SVG sources. When omitted, a 1x1 transparent PNG is generated.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop fallback=/path/to/fallback.png" + ], + "readback": "n/a (SVG-only)", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/picture.json b/schemas/help/_shared/picture.json new file mode 100644 index 000000000..ebd941138 --- /dev/null +++ b/schemas/help/_shared/picture.json @@ -0,0 +1,67 @@ +{ + "$schema": "../_schema.json", + "element": "picture", + "shared_base": true, + "properties": { + "alt": { + "type": "string", + "description": "alternative text (DocProperties.Description). Defaults to the source file name on add. Aliases: alttext, description.", + "aliases": [ + "altText", + "alttext", + "description" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop alt=\"Logo\"", + "--prop alt=\"Company logo\"" + ], + "readback": "string", + "enforcement": "report" + }, + "contentType": { + "type": "string", + "description": "OOXML content-type of the embedded image part (e.g. `image/png`, `image/jpeg`). Read from the package part referenced by the BlipFill embed relationship.", + "add": false, + "set": false, + "get": true, + "readback": "MIME-style content-type string from the image part", + "enforcement": "report" + }, + "fileSize": { + "type": "number", + "description": "embedded image file size in bytes (length of the image part stream).", + "add": false, + "set": false, + "get": true, + "readback": "byte length of the embedded image part", + "enforcement": "report" + }, + "relId": { + "type": "string", + "description": "relationship id of the embedded image part (rId-style token). Required input for `get --save ` binary extraction.", + "add": false, + "set": false, + "get": true, + "readback": "relationship id token", + "enforcement": "report" + }, + "src": { + "type": "string", + "description": "image source (file path, URL, data-URI); accepted on add/set only. Get does NOT surface this key; the embedded relationship id is exposed under a separate Format[\"relId\"] key.", + "aliases": [ + "path" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop src=/path/to/image.png" + ], + "readback": "add/set-only input; not echoed by Get. Use Format[\"relId\"] to inspect the embedded image relationship.", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/picture.pptx-xlsx.json b/schemas/help/_shared/picture.pptx-xlsx.json new file mode 100644 index 000000000..798e44454 --- /dev/null +++ b/schemas/help/_shared/picture.pptx-xlsx.json @@ -0,0 +1,86 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "crop": { + "type": "string", + "description": "Crop in percent (0-100). 1 value = symmetric, 2 values = vertical,horizontal, 4 values = left,top,right,bottom.", + "add": true, + "set": true, + "examples": [ + "--prop crop=10", + "--prop crop=5,10", + "--prop crop=10,5,10,5" + ], + "enforcement": "report", + "get": true, + "readback": "as emitted by handler (per-format details vary)" + }, + "cropBottom": { + "type": "string", + "description": "Crop from bottom edge as percent (0-100). Aliases: cropbottom.", + "aliases": [ + "cropbottom" + ], + "add": true, + "set": true, + "examples": [ + "--prop cropBottom=10" + ], + "enforcement": "report" + }, + "cropLeft": { + "type": "string", + "description": "Crop from left as fraction (<=1) or percent (>1). E.g. cropLeft=0.1 or cropLeft=10 both mean 10%.", + "add": true, + "set": true, + "examples": [ + "--prop cropLeft=0.1", + "--prop cropLeft=10" + ], + "enforcement": "report", + "aliases": [ + "cropleft" + ] + }, + "cropRight": { + "type": "string", + "description": "Crop from right edge as percent (0-100). Aliases: cropright.", + "aliases": [ + "cropright" + ], + "add": true, + "set": true, + "examples": [ + "--prop cropRight=10" + ], + "enforcement": "report" + }, + "cropTop": { + "type": "string", + "description": "Crop from top edge as percent (0-100). Aliases: croptop.", + "aliases": [ + "croptop" + ], + "add": true, + "set": true, + "examples": [ + "--prop cropTop=10" + ], + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "Override the auto-generated 'Picture {id}' label on cNvPr @name.", + "add": true, + "set": false, + "examples": [ + "--prop name=\"hero-image\"", + "--prop name=\"Hero Image\"" + ], + "enforcement": "report", + "get": true, + "readback": "shape name string" + } + } +} diff --git a/schemas/help/_shared/root-metadata.json b/schemas/help/_shared/root-metadata.json new file mode 100644 index 000000000..d816c769d --- /dev/null +++ b/schemas/help/_shared/root-metadata.json @@ -0,0 +1,287 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "extended.applicationVersion": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "docProps/app.xml AppVersion field.", + "readback": "version string", + "enforcement": "report" + }, + "extended.characters": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "docProps/app.xml Characters count.", + "readback": "integer", + "enforcement": "report" + }, + "extended.company": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "docProps/app.xml Company field.", + "readback": "company name", + "enforcement": "report" + }, + "extended.lines": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "docProps/app.xml Lines count.", + "readback": "integer", + "enforcement": "report" + }, + "extended.manager": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "docProps/app.xml Manager field.", + "readback": "manager name", + "enforcement": "report" + }, + "extended.pages": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "docProps/app.xml Pages count.", + "readback": "integer", + "enforcement": "report" + }, + "extended.paragraphs": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "docProps/app.xml Paragraphs count.", + "readback": "integer", + "enforcement": "report" + }, + "extended.template": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "docProps/app.xml Template field.", + "readback": "template name", + "enforcement": "report" + }, + "extended.totalTime": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "docProps/app.xml TotalTime field (minutes).", + "readback": "integer minutes", + "enforcement": "report" + }, + "extended.words": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "docProps/app.xml Words count.", + "readback": "integer", + "enforcement": "report" + }, + "subject": { + "type": "string", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop subject=Finance" + ], + "readback": "subject string", + "enforcement": "report" + }, + "theme.color.accent1": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme accent color 1.", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.accent2": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme accent color 2.", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.accent3": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme accent color 3.", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.accent4": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme accent color 4.", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.accent5": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme accent color 5.", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.accent6": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme accent color 6.", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.dk1": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme color slot dk1 (dark 1 / default text).", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.dk2": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme color slot dk2 (dark 2).", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.folHlink": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme followed-hyperlink color.", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.hlink": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme hyperlink color.", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.lt1": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme color slot lt1 (light 1 / default background).", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.color.lt2": { + "type": "color", + "add": false, + "set": true, + "get": true, + "description": "theme color slot lt2 (light 2).", + "readback": "#RRGGBB or scheme reference", + "enforcement": "report" + }, + "theme.colorScheme": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "color scheme name (a:clrScheme/@name).", + "readback": "color scheme name", + "enforcement": "report" + }, + "theme.font.major.eastAsia": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "major (heading) East Asian typeface.", + "readback": "font family name", + "enforcement": "report" + }, + "theme.font.major.latin": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "major (heading) Latin typeface.", + "readback": "font family name", + "enforcement": "report" + }, + "theme.font.minor.eastAsia": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "minor (body) East Asian typeface.", + "readback": "font family name", + "enforcement": "report" + }, + "theme.font.minor.latin": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "minor (body) Latin typeface.", + "readback": "font family name", + "enforcement": "report" + }, + "theme.fontScheme": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "font scheme name (a:fontScheme/@name).", + "readback": "font scheme name", + "enforcement": "report" + }, + "theme.formatScheme": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "format scheme name (a:fmtScheme/@name).", + "readback": "format scheme name", + "enforcement": "report" + }, + "theme.name": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "theme display name (a:theme/@name).", + "readback": "theme name string", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/run.docx-pptx.json b/schemas/help/_shared/run.docx-pptx.json new file mode 100644 index 000000000..1f52649a1 --- /dev/null +++ b/schemas/help/_shared/run.docx-pptx.json @@ -0,0 +1,64 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "effective.bold": { + "type": "bool", + "description": "resolved bold inherited from placeholder→layout→master→presentation defaults. Suppressed when 'bold' is set directly on the run.", + "add": false, + "set": false, + "get": true, + "readback": "true/false", + "enforcement": "report" + }, + "effective.color": { + "type": "color", + "description": "resolved text color inherited from placeholder→layout→master→presentation defaults. Suppressed when 'color' is set directly on the run.", + "add": false, + "set": false, + "get": true, + "readback": "#-prefixed uppercase hex (scheme colors pass through)", + "enforcement": "report" + }, + "effective.size": { + "type": "font-size", + "description": "inheritance-resolved font size (read-only). Surfaced when the run does not set 'size' directly; resolved through run style → paragraph style → docDefaults.", + "add": false, + "set": false, + "get": true, + "readback": "unit-qualified, e.g. \"14pt\"", + "enforcement": "report" + }, + "underline": { + "type": "enum", + "description": "underline style. Common values: single, double, dotted, dash, wave, none.", + "values": [ + "single", + "double", + "dotted", + "dash", + "wave", + "none", + "thick", + "dottedHeavy", + "dashLong", + "dashLongHeavy", + "dashDotHeavy", + "wavyHeavy", + "wavyDouble" + ], + "aliases": [ + "font.underline" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop underline=single", + "--prop underline=double" + ], + "readback": "underline style name", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/run.docx-xlsx.json b/schemas/help/_shared/run.docx-xlsx.json new file mode 100644 index 000000000..75320ac20 --- /dev/null +++ b/schemas/help/_shared/run.docx-xlsx.json @@ -0,0 +1,30 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "subscript": { + "type": "bool", + "description": "vertical alignment = subscript. Mutually exclusive with superscript.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop subscript=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "superscript": { + "type": "bool", + "description": "vertical alignment = superscript. Mutually exclusive with subscript.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop superscript=true" + ], + "readback": "true | false", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/_shared/run.json b/schemas/help/_shared/run.json new file mode 100644 index 000000000..2f0082e5d --- /dev/null +++ b/schemas/help/_shared/run.json @@ -0,0 +1,102 @@ +{ + "$schema": "../_schema.json", + "element": "run", + "shared_base": true, + "properties": { + "bold": { + "type": "bool", + "aliases": [ + "font.bold" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop bold=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "color": { + "type": "color", + "aliases": [ + "font.color" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop color=#FF0000", + "--prop color=FF0000", + "--prop color=red" + ], + "readback": "#RRGGBB uppercase", + "enforcement": "strict" + }, + "font": { + "type": "string", + "description": "bare font family — write-only convenience that sets ASCII+HighAnsi+EastAsia to the same value. Get normalizes the readback to per-script canonical keys (font.latin / font.ea / font.cs) so a get→set round-trip preserves divergent slot values.", + "aliases": [ + "fontname", + "fontFamily", + "font.name" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop font=Calibri", + "--prop font=\"Arial\"", + "--prop font=\"Times New Roman\"" + ], + "readback": "see font.latin / font.ea / font.cs", + "enforcement": "strict" + }, + "italic": { + "type": "bool", + "aliases": [ + "font.italic" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop italic=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "size": { + "type": "font-size", + "aliases": [ + "fontsize", + "fontSize", + "font.size" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop size=11", + "--prop size=14", + "--prop size=14pt", + "--prop size=10.5pt" + ], + "readback": "unit-qualified, e.g. \"14pt\"", + "enforcement": "strict" + }, + "text": { + "type": "string", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop text=\"bold word\"", + "--prop text=\"word\"", + "--prop text=\"run content\"" + ], + "readback": "plain text of run", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/_shared/shape.json b/schemas/help/_shared/shape.json new file mode 100644 index 000000000..ead34ffd1 --- /dev/null +++ b/schemas/help/_shared/shape.json @@ -0,0 +1,301 @@ +{ + "$schema": "../_schema.json", + "element": "shape", + "shared_base": true, + "properties": { + "align": { + "type": "string", + "description": "Paragraph alignment: 'left' / 'center' (c/ctr) / 'right' (r) / 'justify'.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop align=center" + ], + "enforcement": "report" + }, + "bold": { + "type": "bool", + "description": "Bold runs. Bare alias of font.bold.", + "add": true, + "set": true, + "examples": [ + "--prop bold=true" + ], + "enforcement": "report", + "aliases": [ + "font.bold" + ], + "get": true, + "readback": "as emitted by handler (per-format details vary)" + }, + "color": { + "type": "color", + "description": "Text color. Bare alias of font.color.", + "add": true, + "set": true, + "examples": [ + "--prop color=#FF0000", + "--prop color=0000FF" + ], + "enforcement": "report", + "aliases": [ + "font.color" + ], + "get": true, + "readback": "as emitted by handler (per-format details vary)" + }, + "fill": { + "type": "color", + "description": "Solid fill color, or 'none' for no fill (text-only shapes route effects to text-level rPr).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop fill=#FFFF00", + "--prop fill=none", + "--prop fill=FF0000", + "--prop fill=#FF0000", + "--prop fill=red", + "--prop fill=accent1" + ], + "readback": "#-prefixed uppercase hex", + "enforcement": "report", + "aliases": [ + "background" + ] + }, + "flipH": { + "type": "bool", + "aliases": [ + "flipHorizontal" + ], + "description": "Flip horizontally (Office-API alias of flip=h).", + "add": true, + "set": true, + "examples": [ + "--prop flipH=true" + ], + "enforcement": "report" + }, + "flipV": { + "type": "bool", + "aliases": [ + "flipVertical" + ], + "description": "Flip vertically (Office-API alias of flip=v).", + "add": true, + "set": true, + "examples": [ + "--prop flipV=true" + ], + "enforcement": "report" + }, + "font": { + "type": "string", + "description": "default font family for shape text. Bare 'font' targets Latin + EastAsian; for per-script control (Japanese / Korean / Arabic) use font.latin, font.ea, or font.cs.", + "aliases": [ + "font.name" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font=Arial" + ], + "readback": "font name", + "enforcement": "report" + }, + "glow": { + "type": "string", + "description": "glow effect. Pass a color (e.g. '4472C4') or 'true' (defaults to accent blue).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop glow=#4472C4", + "--prop glow=4472C4", + "--prop glow=true" + ], + "readback": "color hex string", + "enforcement": "report" + }, + "italic": { + "type": "bool", + "description": "Italic runs. Bare alias of font.italic.", + "add": true, + "set": true, + "examples": [ + "--prop italic=true" + ], + "enforcement": "report", + "aliases": [ + "font.italic" + ], + "get": true, + "readback": "as emitted by handler (per-format details vary)" + }, + "line": { + "type": "string", + "description": "Outline color (or 'none'). Form: 'color[:width[:style]]', e.g. 'FF0000:1.5:dash'. width in points; style: solid|dash|dot|dashdot|longdash.", + "aliases": [ + "border", + "linecolor", + "lineColor" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop line=#000000", + "--prop line=FF0000:1.5", + "--prop line=none", + "--prop line=000000" + ], + "readback": "color or color:width", + "enforcement": "report" + }, + "margin": { + "type": "length", + "description": "uniform internal padding (text inset) for shape body.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop margin=4", + "--prop margin=0.1in" + ], + "readback": "n/a", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "Override the auto-generated 'Shape {id}' label on cNvPr @name.", + "add": true, + "set": true, + "examples": [ + "--prop name=\"banner\"", + "--prop name=MyShape" + ], + "enforcement": "report", + "get": true, + "readback": "shape name string (cNvPr @name)" + }, + "reflection": { + "type": "string", + "description": "reflection effect. Accepts 'true' to enable a default reflection.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop reflection=true" + ], + "readback": "n/a", + "enforcement": "report" + }, + "rotation": { + "type": "string", + "description": "Rotation in degrees (positive = clockwise). Stored OOXML-internal as 60000ths of a degree on Transform2D @rot.", + "aliases": [ + "rot", + "rotate" + ], + "add": true, + "set": true, + "examples": [ + "--prop rotation=45" + ], + "enforcement": "report", + "get": true, + "readback": "as emitted by handler (per-format details vary)" + }, + "shadow": { + "type": "string", + "description": "outer shadow effect. Pass a color (e.g. '000000') or 'true' (defaults to black). Routed to text-level rPr for text-only shapes.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop shadow=#808080", + "--prop shadow=none", + "--prop shadow=000000", + "--prop shadow=true" + ], + "readback": "color hex string", + "enforcement": "report" + }, + "size": { + "type": "font-size", + "description": "font size", + "aliases": [ + "fontSize", + "fontsize", + "font.size" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop size=14", + "--prop size=14pt", + "--prop size=10.5pt" + ], + "readback": "unit-qualified string, e.g. \"14pt\"", + "enforcement": "strict" + }, + "softEdge": { + "type": "string", + "aliases": [ + "softedge" + ], + "description": "Soft edge radius, or 'none' to clear.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop softEdge=5", + "--prop softEdge=4pt" + ], + "readback": "unit-qualified string in points, e.g. \"5pt\"", + "enforcement": "report" + }, + "text": { + "type": "string", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop text=\"Note\"", + "--prop text=\"Hello\"" + ], + "readback": "plain text content of the shape", + "enforcement": "strict" + }, + "underline": { + "type": "string", + "description": "Underline style: 'true'/'single'/'sng', 'double'/'dbl', 'none'/'false'. Bare alias of font.underline.", + "add": true, + "set": true, + "examples": [ + "--prop underline=single" + ], + "enforcement": "report", + "aliases": [ + "font.underline" + ], + "get": true, + "readback": "as emitted by handler (per-format details vary)" + }, + "valign": { + "type": "string", + "description": "Vertical anchor: 'top' (t) / 'center' (ctr/middle/c/m) / 'bottom' (b).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop valign=middle" + ], + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/table-cell.json b/schemas/help/_shared/table-cell.json new file mode 100644 index 000000000..5d431fe09 --- /dev/null +++ b/schemas/help/_shared/table-cell.json @@ -0,0 +1,130 @@ +{ + "$schema": "../_schema.json", + "element": "table-cell", + "shared_base": true, + "properties": { + "border.all": { + "type": "string", + "description": "all four cell edges. Format: 'WIDTH[ DASH][ COLOR]' (e.g. '1pt solid FF0000') or 'STYLE;WIDTH;COLOR[;DASH]' (style ignored — kept for docx parity). DASH ∈ solid|dot|dash|lgDash|dashDot|sysDot|sysDash. Use 'none' to clear. Alias: border. Stored as a:lnL/lnR/lnT/lnB on a:tcPr. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "aliases": [ + "border" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.all=\"single;1pt;FF0000\"", + "--prop border.all=\"1pt solid FF0000\"", + "--prop border=none" + ], + "enforcement": "report" + }, + "border.bottom": { + "type": "string", + "description": "bottom border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR' (SIZE is in 1/8 pt units).", + "add": false, + "set": true, + "get": false, + "examples": [ + "--prop border.bottom=\"single;1pt;808080\"", + "--prop border.bottom=\"1pt solid 808080\"", + "--prop border.bottom=\"double;6;0000FF\"" + ], + "enforcement": "report" + }, + "border.left": { + "type": "string", + "description": "left border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR' (SIZE is in 1/8 pt units).", + "add": false, + "set": true, + "get": false, + "examples": [ + "--prop border.left=\"single;1pt;808080\"", + "--prop border.left=\"1pt solid 808080\"" + ], + "enforcement": "report" + }, + "border.right": { + "type": "string", + "description": "right border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR' (SIZE is in 1/8 pt units).", + "add": false, + "set": true, + "get": false, + "examples": [ + "--prop border.right=\"single;1pt;808080\"", + "--prop border.right=\"1pt solid 808080\"" + ], + "enforcement": "report" + }, + "border.tl2br": { + "type": "string", + "description": "diagonal from top-left to bottom-right (a:lnTlToBr). Format same as border.all. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient. Add/Set only — Get does not surface diagonal borders today.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.tl2br=\"single;1pt;FF0000\"", + "--prop border.tl2br=\"1pt solid FF0000\"" + ], + "enforcement": "report" + }, + "border.top": { + "type": "string", + "description": "top border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR' (SIZE is in 1/8 pt units).", + "add": false, + "set": true, + "get": false, + "examples": [ + "--prop border.top=\"single;2pt;000000\"", + "--prop border.top=\"2pt solid 000000\"" + ], + "enforcement": "report" + }, + "border.tr2bl": { + "type": "string", + "description": "diagonal from top-right to bottom-left (a:lnBlToTr). Format same as border.all. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient. Add/Set only — Get does not surface diagonal borders today.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.tr2bl=\"single;1pt;FF0000\"", + "--prop border.tr2bl=\"1pt solid FF0000\"" + ], + "enforcement": "report" + }, + "fill": { + "type": "color", + "description": "cell background fill. Accepts a solid color (hex, named, rgb(...)), 'none' for explicit no-fill, or a gradient string 'COLOR1-COLOR2[-ANGLE]' (e.g. 'FF0000-0000FF-90'). Stored on the cell's properties element using each format's native shading/fill primitives. Scheme color names (accent1, dk1, lt2, …) are supported in pptx only.", + "aliases": [ + "background", + "shd", + "shading" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop fill=FFFF00", + "--prop fill=#FF0000", + "--prop fill=red", + "--prop fill=none", + "--prop fill=\"FF0000-0000FF-90\"", + "--prop fill=\"gradient;FF0000;0000FF;90\"" + ], + "readback": "#RRGGBB uppercase, 'gradient' (with separate 'gradient' key), or 'image' for picture fill", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "single-run text content placed in a fresh paragraph.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop text=\"Hello\"" + ], + "readback": "concatenated run text", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/_shared/table-row.json b/schemas/help/_shared/table-row.json new file mode 100644 index 000000000..f44c14736 --- /dev/null +++ b/schemas/help/_shared/table-row.json @@ -0,0 +1,32 @@ +{ + "$schema": "../_schema.json", + "element": "table-row", + "shared_base": true, + "properties": { + "cols": { + "type": "int", + "description": "asserts the new row's cell count matches the table grid. The row is structurally locked to the existing — to widen, add a column first (or set gridSpan on cell 1 to merge into a wider span). Defaults to the existing grid column count when omitted.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop cols=2" + ], + "readback": "n/a (structural — cell count surfaces via DocumentNode.Children, not Format)", + "enforcement": "strict" + }, + "height": { + "type": "length", + "description": "row height in EMU-parseable length. Defaults to first-row height or ~1cm.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop height=1cm", + "--prop height=500" + ], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/table.docx-pptx.json b/schemas/help/_shared/table.docx-pptx.json new file mode 100644 index 000000000..aee7583b4 --- /dev/null +++ b/schemas/help/_shared/table.docx-pptx.json @@ -0,0 +1,153 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "border.all": { + "type": "string", + "description": "shorthand: applies the border to every edge of every cell. PPT OOXML has no table-level border element — this fans out to per-cell a:lnL/lnR/lnT/lnB. Format: 'WIDTH[ DASH][ COLOR]' space-separated (e.g. '1pt solid FF0000') or 'STYLE;WIDTH;COLOR[;DASH]' semicolon form (style is ignored — kept for docx parity). DASH ∈ solid|dot|dash|lgDash|dashDot|sysDot|sysDash. Use 'none' to clear. Alias: border. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "aliases": [ + "border" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.all=\"single;1pt;FF0000\"", + "--prop border.all=\"1pt solid FF0000\"", + "--prop border=\"single;1pt;000000\"", + "--prop border.all=none" + ], + "enforcement": "report" + }, + "border.bottom": { + "type": "string", + "description": "outer bottom border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR'. Add/Set only — read per-cell.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.bottom=\"single;2pt;000000\"", + "--prop border.bottom=\"2pt solid 000000\"", + "--prop border.bottom=\"double;6;0000FF\"" + ], + "enforcement": "report" + }, + "border.horizontal": { + "type": "string", + "description": "inside-horizontal dividers (between rows). Fans out to bottom of rows 1..N-1 plus top of rows 2..N. PPT has no native inside-border element. Alias: border.insideH. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "aliases": [ + "border.insideh", + "border.insideH" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.horizontal=\"single;1pt;CCCCCC\"", + "--prop border.horizontal=\"1pt solid CCCCCC\"" + ], + "enforcement": "report" + }, + "border.left": { + "type": "string", + "description": "outer left edge: applies to the left of column-1 cells in every row only. Format same as border.all. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.left=\"single;1pt;808080\"", + "--prop border.left=\"1pt solid 808080\"" + ], + "enforcement": "report" + }, + "border.right": { + "type": "string", + "description": "outer right edge: applies to the right of last-column cells in every row only. Format same as border.all. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.right=\"single;1pt;808080\"", + "--prop border.right=\"1pt solid 808080\"" + ], + "enforcement": "report" + }, + "border.top": { + "type": "string", + "description": "outer top border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR' (SIZE is in 1/8 pt units). Add/Set only — table-level border readback is not surfaced today; inspect per-cell border.top instead.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.top=\"single;2pt;000000\"", + "--prop border.top=\"2pt solid 000000\"" + ], + "enforcement": "report" + }, + "border.vertical": { + "type": "string", + "description": "inside-vertical dividers (between columns). Fans out to right of cols 1..M-1 plus left of cols 2..M. Alias: border.insideV. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "aliases": [ + "border.insidev", + "border.insideV" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.vertical=\"single;1pt;CCCCCC\"", + "--prop border.vertical=\"1pt solid CCCCCC\"" + ], + "enforcement": "report" + }, + "cols": { + "type": "int", + "description": "number of columns (ignored if 'data' is supplied).", + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop cols=3" + ], + "readback": "integer column count from first row", + "enforcement": "strict" + }, + "data": { + "type": "string", + "description": "inline CSV-ish data ('H1,H2;R1C1,R1C2') or CSV file/URL/data-URI resolvable by FileSource.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop data=\"A,B;1,2\"" + ], + "readback": "n/a (seeds cells at Add time)", + "enforcement": "strict" + }, + "rows": { + "type": "int", + "description": "number of rows (ignored if 'data' is supplied).", + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop rows=3" + ], + "readback": "integer row count", + "enforcement": "strict" + }, + "width": { + "type": "string", + "description": "table width in twips (Dxa) or percent ('50%' → Pct).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop width=10cm", + "--prop width=9000" + ], + "readback": "Dxa twips or pct50ths", + "enforcement": "report" + } + } +} diff --git a/schemas/help/_shared/table.json b/schemas/help/_shared/table.json new file mode 100644 index 000000000..1048dbb57 --- /dev/null +++ b/schemas/help/_shared/table.json @@ -0,0 +1,37 @@ +{ + "$schema": "../_schema.json", + "element": "table", + "shared_base": true, + "properties": { + "style": { + "type": "string", + "description": "table style name or GUID (accepted aliases: tableStyle, tableStyleId). Valid names: medium1..4, light1..3, dark1..2, none, or a direct {GUID}.", + "values": [ + "medium1", + "medium2", + "medium3", + "medium4", + "light1", + "light2", + "light3", + "dark1", + "dark2", + "none" + ], + "aliases": [ + "tableStyle", + "tableStyleId" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop style=medium2", + "--prop style=light1", + "--prop style=dark1" + ], + "readback": "style name when resolvable, else GUID", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/_shared/table.pptx-xlsx.json b/schemas/help/_shared/table.pptx-xlsx.json new file mode 100644 index 000000000..312ae6d97 --- /dev/null +++ b/schemas/help/_shared/table.pptx-xlsx.json @@ -0,0 +1,19 @@ +{ + "$schema": "../_schema.json", + "shared_base": true, + "properties": { + "name": { + "type": "string", + "description": "NonVisualDrawingProperties Name (used for stable @name addressing).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop name=SalesData", + "--prop name=Summary" + ], + "readback": "name string", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/docx/abstractNum.json b/schemas/help/docx/abstractNum.json new file mode 100644 index 000000000..32a8cf1dc --- /dev/null +++ b/schemas/help/docx/abstractNum.json @@ -0,0 +1,268 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "abstractNum", + "parent": "numbering", + "addParent": "/numbering", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": ["/numbering/abstractNum[@id=N]"], + "positional": ["/numbering/abstractNum[N]"] + }, + "note": "Numbering template (). Defines what a list LOOKS like — 9 levels, each with its own number format, marker text, indent, start value, and marker run properties. Reusable: many instances can share one abstractNum. To apply the list to paragraphs, create a pointing at this abstractNumId (--type num --prop abstractNumId=N), then reference that numId on the paragraph or style (--prop numId=N --prop ilvl=L). Top-level format/text/start/indent are shorthand for level0.*; per-level customization uses level.* (N = 0..8). If a top-level format is set, every level inherits it instead of cycling through decimal/lowerLetter/lowerRoman.", + "properties": { + "id": { + "type": "int", + "description": "abstractNumId (w:abstractNumId, unique within /numbering). Omit for auto-assignment. Renaming after Add would orphan every referencing it; not supported.", + "add": true, + "set": false, + "get": true, + "examples": ["--prop id=5"], + "readback": "integer abstractNumId", + "enforcement": "strict" + }, + "type": { + "type": "enum", + "values": ["hybridMultilevel", "multilevel", "singleLevel"], + "aliases": { + "hybridMultilevel": ["hybrid"], + "multilevel": ["multi"], + "singleLevel": ["single"] + }, + "description": "multiLevelType (w:multiLevelType). Default: hybridMultilevel. Hybrid is what Word writes for ad-hoc lists; singleLevel locks bullets/numbers to level 0.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop type=hybridMultilevel"], + "readback": "hybridMultilevel | multilevel | singleLevel", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "human-readable name (w:name). Optional; Word shows this in the Numbering dialog.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop name=\"Chapter outline\""], + "readback": "AbstractNumDefinitionName.Val", + "enforcement": "report" + }, + "styleLink": { + "type": "string", + "description": "back-reference to a numbering style (w:styleLink). Optional.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop styleLink=MyListStyle"], + "readback": "StyleLink.Val", + "enforcement": "report" + }, + "numStyleLink": { + "type": "string", + "description": "link to another abstractNum via numbering style (w:numStyleLink). Optional.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop numStyleLink=OutlineList"], + "readback": "NumberingStyleLink.Val", + "enforcement": "report" + }, + "format": { + "type": "string", + "description": "numFmt for level 0 (and propagated to all 9 levels if no per-level override). Common values: decimal, decimalZero, upperRoman, lowerRoman, upperLetter, lowerLetter, ordinal, cardinalText, ordinalText, bullet, chineseCounting, chineseLegalSimplified, japaneseCounting, koreanCounting, hex. Top-level bare 'format' is shorthand for level0.format; setting it also defaults every other level to the same format instead of the decimal/lowerLetter/lowerRoman cycle.", + "aliases": ["fmt", "numFmt"], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop format=decimal", + "--prop format=bullet", + "--prop format=chineseCounting" + ], + "readback": "n/a (surfaces on /numbering/abstractNum[@id=N]/level[L])", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "lvlText for level 0. Use %N as placeholder for level N's running counter (e.g. '%1.', '%1.%2', '第%1章'). Bullet marker default: •. Top-level bare 'text' is shorthand for level0.text.", + "aliases": ["lvlText"], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop text=\"%1.\"", + "--prop text=\"第%1章\"", + "--prop lvlText=\"%1.%2\"" + ], + "readback": "n/a (surfaces on /numbering/abstractNum[@id=N]/level[L])", + "enforcement": "report" + }, + "start": { + "type": "int", + "description": "starting number for level 0. Default 1. Shorthand for level0.start.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop start=5"], + "readback": "n/a (surfaces on /numbering/abstractNum[@id=N]/level[L])", + "enforcement": "report" + }, + "indent": { + "type": "int", + "description": "left indent in twips for level 0. Default (level+1)*720. Shorthand for level0.indent.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop indent=720"], + "readback": "n/a (surfaces on /numbering/abstractNum[@id=N]/level[L])", + "enforcement": "report" + }, + "level.format": { + "type": "string", + "description": "numFmt for level N (N = 0..8). Same vocabulary as bare 'format'. Per-level override; only set the levels you actually use, the rest fall back to decimal/lowerLetter/lowerRoman cycle (or whatever 'format' propagates).", + "aliases": ["level.fmt", "level.numFmt"], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop level0.format=upperRoman --prop level1.format=decimal" + ], + "readback": "n/a (surfaces on level[N])", + "enforcement": "report" + }, + "level.text": { + "type": "string", + "description": "lvlText for level N. Same %N placeholder syntax.", + "aliases": ["level.lvlText"], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop level0.text=\"%1.\" --prop level1.text=\"%1.%2\"" + ], + "readback": "n/a (surfaces on level[N])", + "enforcement": "report" + }, + "level.start": { + "type": "int", + "description": "starting number for level N.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop level3.start=1"], + "readback": "n/a (surfaces on level[N])", + "enforcement": "report" + }, + "level.indent": { + "type": "int", + "description": "left indent in twips for level N.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop level0.indent=720"], + "readback": "n/a (surfaces on level[N])", + "enforcement": "report" + }, + "level.hanging": { + "type": "int", + "description": "hanging indent in twips for level N. Default 360.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop level0.hanging=360"], + "readback": "n/a", + "enforcement": "report" + }, + "level.justification": { + "type": "enum", + "values": ["left", "center", "right"], + "aliases": ["level.jc"], + "description": "lvlJc — marker alignment within the indent area. Default left.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop level0.justification=left"], + "readback": "n/a", + "enforcement": "report" + }, + "level.suff": { + "type": "enum", + "values": ["tab", "space", "nothing"], + "description": "what comes between the marker and the text content. Default tab.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop level0.suff=space"], + "readback": "n/a", + "enforcement": "report" + }, + "level.font": { + "type": "string", + "description": "marker font (rPr/rFonts on the level). Useful for bullet glyphs that need Symbol or Wingdings.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop level0.font=Symbol"], + "readback": "n/a", + "enforcement": "report" + }, + "level.size": { + "type": "font-size", + "description": "marker font size (rPr/sz on the level). Bare number = pt.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop level0.size=14"], + "readback": "n/a", + "enforcement": "report" + }, + "level.color": { + "type": "color", + "description": "marker color (rPr/color on the level).", + "add": true, + "set": false, + "get": false, + "examples": ["--prop level0.color=#FF0000"], + "readback": "n/a", + "enforcement": "report" + }, + "level.bold": { + "type": "bool", + "description": "bold marker.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop level0.bold=true"], + "readback": "n/a", + "enforcement": "report" + }, + "level.italic": { + "type": "bool", + "description": "italic marker.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop level0.italic=true"], + "readback": "n/a", + "enforcement": "report" + }, + "abstractNumId": { + "type": "int", + "description": "readback alias for id (matches the w:abstractNumId attribute name).", + "add": false, + "set": false, + "get": true, + "readback": "integer", + "enforcement": "report" + } + }, + "children": [ + { "element": "level", "pathSegment": "level", "cardinality": "0..9" } + ] +} diff --git a/schemas/help/docx/body.json b/schemas/help/docx/body.json new file mode 100644 index 000000000..1d5d7d5b8 --- /dev/null +++ b/schemas/help/docx/body.json @@ -0,0 +1,24 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "body", + "parent": "document", + "container": true, + "operations": { + "add": false, + "set": false, + "get": true, + "query": true, + "remove": false + }, + "paths": { + "positional": ["/body"] + }, + "note": "Main content container. Get returns the ordered stream of paragraphs, tables, sections. Mutate via child paths (/body/p[N], /body/tbl[N], /body/section[N]).", + "children": [ + { "element": "paragraph", "pathSegment": "p", "cardinality": "0..n" }, + { "element": "table", "pathSegment": "tbl", "cardinality": "0..n" }, + { "element": "section", "pathSegment": "section", "cardinality": "0..n" }, + { "element": "sdt", "pathSegment": "sdt", "cardinality": "0..n" } + ] +} diff --git a/schemas/help/docx/bookmark.json b/schemas/help/docx/bookmark.json new file mode 100644 index 000000000..caf9b13ef --- /dev/null +++ b/schemas/help/docx/bookmark.json @@ -0,0 +1,43 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "bookmark", + "parent": "body|paragraph", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": ["/bookmark[@name=NAME]"], + "positional": ["/bookmark[N]"] + }, + "note": "Bookmarks are BookmarkStart/End pairs. Name must be addressable: no whitespace, no '/[]\"', no leading '@' or single quote. Duplicate names rejected at Add.", + "properties": { + "name": { + "type": "string", + "description": "bookmark name (required). Letters, digits, '.', '_', '-' only.", + "add": true, "set": true, "get": true, + "examples": ["--prop name=chapter1"], + "readback": "name as stored on BookmarkStart", + "enforcement": "strict" + }, + "text": { + "type": "string", + "description": "optional bookmark-covered text. Without this, only an empty Start/End pair is inserted.", + "add": true, "set": false, "get": false, + "examples": ["--prop text=\"Chapter 1 title\""], + "readback": "not a distinct key — lives on wrapped runs", + "enforcement": "strict" + }, + "id": { + "type": "string", + "description": "OOXML bookmark id (w:bookmarkStart/@w:id). Assigned by the writer; surfaces only on Get/Query.", + "add": false, "set": false, "get": true, + "readback": "numeric bookmark id as stored on BookmarkStart", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/chart-axis.json b/schemas/help/docx/chart-axis.json new file mode 100644 index 000000000..455a2fa8b --- /dev/null +++ b/schemas/help/docx/chart-axis.json @@ -0,0 +1,24 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "chart-axis", + "parent": "chart", + "operations": { + "add": false, + "set": true, + "get": true, + "remove": false + }, + "note": "Mirror of pptx/chart-axis. Axes are created/destroyed implicitly by chartType changes — no direct Add/Remove. At chart-creation time, configure via the chart's axis* props (axismin/axismax/axistitle/axisfont/…); chart-axis covers post-creation only. Known gaps: `labelFont` writes the title run (not tick labels); `lineWidth`/`lineDash` apply to all plot-area series — use chart-series for series-specific line styling.", + "addressing": { + "key": "role", + "pathForm": "/chart[N]/axis[@role=ROLE]", + "keyValues": [ + "category", + "value", + "value2", + "series" + ] + }, + "extends": "_shared/chart-axis" +} diff --git a/schemas/help/docx/chart-series.json b/schemas/help/docx/chart-series.json new file mode 100644 index 000000000..2813f3db0 --- /dev/null +++ b/schemas/help/docx/chart-series.json @@ -0,0 +1,20 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "chart-series", + "parent": "chart", + "operations": { + "add": true, + "set": true, + "get": true, + "remove": true + }, + "paths": { + "positional": [ + "/chart[N]/series[K]", + "/body/p[N]/chart[M]/series[K]" + ] + }, + "note": "At Add time, series pass as dotted props on the parent chart (series1.name, series1.values, series1.color, series1.categories); this schema is per-series Set/Get after creation. Combo charts (mixed chartType / secondary axis) are unsupported — create separate charts. `lineStyle` is not a key (rejected as UNSUPPORTED — use lineWidth + lineDash).", + "extends": "_shared/chart-series" +} diff --git a/schemas/help/docx/chart.json b/schemas/help/docx/chart.json new file mode 100644 index 000000000..32bc7eb42 --- /dev/null +++ b/schemas/help/docx/chart.json @@ -0,0 +1,42 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "chart", + "parent": "paragraph|body", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/body/p[N]/chart[M]" + ] + }, + "note": "Embedded as inline DrawingML chart (c:chart) or extended chart (cx:chart) depending on chartType. Data via inline spec or per-series props. Mirrors pptx/chart surface. Axis configuration: chart-level axis* props (axismin, axismax, axistitle, axisfont, ...) are Add-time only; for post-creation axis Set/Get use the chart-axis element.", + "extends": [ + "_shared/chart", + "_shared/chart.docx-pptx", + "_shared/chart.docx-xlsx" + ], + "properties": { + "dispUnits": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "value-axis display units token readback (e.g. thousands, millions). Surfaces on the chart node when emitted by the value axis.", + "readback": "display unit token", + "enforcement": "report", + "aliases": [ + "displayunits", + "dispunits" + ], + "examples": [ + "--prop dispunits=thousands" + ] + } + } +} diff --git a/schemas/help/docx/comment.json b/schemas/help/docx/comment.json new file mode 100644 index 000000000..0e3d285d9 --- /dev/null +++ b/schemas/help/docx/comment.json @@ -0,0 +1,63 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "comment", + "parent": "paragraph|run", + "addParent": [ + "/body/p[N]", + "/body/p[N]/r[M]" + ], + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": [ + "/comments/comment[@commentId=N]" + ], + "positional": [ + "/comments/comment[N]" + ] + }, + "note": "Comments live in WordprocessingCommentsPart. Anchor: CommentRangeStart/End surround the target run or paragraph; CommentReference marks the inline anchor.", + "extends": [ + "_shared/comment", + "_shared/comment.docx-pptx" + ], + "properties": { + "id": { + "type": "number", + "description": "OOXML comment id (w:comment/@w:id). Assigned by the writer; surfaces only on Get/Query.", + "add": false, + "set": false, + "get": true, + "readback": "integer comment ID", + "enforcement": "report" + }, + "anchoredTo": { + "type": "string", + "description": "path of the paragraph or run the comment is anchored to (resolved from CommentRangeStart).", + "add": false, + "set": false, + "get": true, + "readback": "path of anchored paragraph/run", + "enforcement": "report" + }, + "runStart": { + "type": "int", + "description": "1-based run index inside the parent paragraph where the comment range starts. N=0 (default) anchors the comment to paragraph start; N>=1 places immediately after the Nth run so dump→batch round-trip restores intra-paragraph anchor positions.", + "aliases": ["runstart"], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop runStart=2" + ], + "readback": "n/a (encoded inline; surfaces via anchoredTo path)", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/document.json b/schemas/help/docx/document.json new file mode 100644 index 000000000..b73392ca1 --- /dev/null +++ b/schemas/help/docx/document.json @@ -0,0 +1,645 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "document", + "container": true, + "operations": { + "add": false, + "set": true, + "get": true, + "query": true, + "remove": false + }, + "paths": { + "positional": [ + "/" + ] + }, + "note": "Root container. Get returns top-level children (body, styles, numbering, headers, footers, etc.). Set exposes core document properties (author/title/subject/keywords/description).", + "children": [ + { + "element": "body", + "pathSegment": "body", + "cardinality": "1" + }, + { + "element": "styles", + "pathSegment": "styles", + "cardinality": "1" + }, + { + "element": "numbering", + "pathSegment": "numbering", + "cardinality": "0..1" + } + ], + "extends": "_shared/root-metadata", + "properties": { + "author": { + "type": "string", + "aliases": [ + "creator" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop author=\"Alice\"" + ], + "readback": "author string", + "enforcement": "report" + }, + "title": { + "type": "string", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop title=\"Report\"" + ], + "readback": "title string", + "enforcement": "report" + }, + "keywords": { + "type": "string", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop keywords=\"tag1,tag2\"" + ], + "readback": "keywords string", + "enforcement": "report" + }, + "description": { + "type": "string", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop description=\"Abstract\"" + ], + "readback": "description string", + "enforcement": "report" + }, + "lastModifiedBy": { + "type": "string", + "aliases": [ + "lastmodifiedby" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop lastModifiedBy=\"Bob\"" + ], + "readback": "last-modified author", + "enforcement": "report" + }, + "category": { + "type": "string", + "description": "document category metadata. Emitted only when present.", + "add": false, + "set": false, + "get": true, + "readback": "category string", + "enforcement": "report" + }, + "revisionNumber": { + "type": "string", + "description": "document save counter from docProps/core.xml (cp:revision). Distinct from tracked-change revisions — see `help docx revision`.", + "add": false, + "set": false, + "get": true, + "readback": "revision number string", + "enforcement": "report" + }, + "created": { + "type": "string", + "description": "creation timestamp (ISO-8601). Emitted only when present.", + "add": false, + "set": false, + "get": true, + "readback": "ISO-8601 timestamp", + "enforcement": "report" + }, + "modified": { + "type": "string", + "description": "last modification timestamp (ISO-8601). Emitted only when present.", + "add": false, + "set": false, + "get": true, + "readback": "ISO-8601 timestamp", + "enforcement": "report" + }, + "protection": { + "type": "enum", + "values": [ + "none", + "readOnly", + "comments", + "trackedChanges", + "forms" + ], + "description": "document protection mode.", + "add": false, + "set": false, + "get": true, + "readback": "protection mode name", + "enforcement": "report" + }, + "protectionEnforced": { + "type": "bool", + "description": "whether document protection is enforced.", + "add": false, + "set": false, + "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "docGrid.type": { + "type": "enum", + "values": [ + "default", + "lines", + "linesAndChars", + "snapToChars" + ], + "description": "document grid type.", + "add": false, + "set": true, + "get": true, + "readback": "grid type token", + "enforcement": "report" + }, + "docGrid.linePitch": { + "type": "number", + "description": "document grid line pitch.", + "add": false, + "set": true, + "get": true, + "readback": "integer", + "enforcement": "report" + }, + "docGrid.charSpace": { + "type": "number", + "description": "document grid char space.", + "add": false, + "set": true, + "get": true, + "readback": "integer", + "enforcement": "report" + }, + "charSpacingControl": { + "type": "enum", + "values": [ + "compressPunctuation", + "compressPunctuationAndJapaneseKana", + "doNotCompress" + ], + "description": "CJK character spacing control.", + "add": false, + "set": true, + "get": true, + "readback": "spacing control token", + "enforcement": "report" + }, + "compatibility.mode": { + "type": "string", + "description": "compatibility mode identifier.", + "add": false, + "set": true, + "get": true, + "readback": "compatibility mode value", + "enforcement": "report" + }, + "docDefaults.font": { + "type": "string", + "description": "default Latin font.", + "aliases": [ + "defaultFont" + ], + "add": false, + "set": true, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "docDefaults.font.eastAsia": { + "type": "string", + "description": "default East Asian font slot.", + "add": false, + "set": true, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "docDefaults.font.hAnsi": { + "type": "string", + "description": "default hAnsi font slot.", + "add": false, + "set": true, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "docDefaults.font.complexScript": { + "type": "string", + "description": "default complex-script font slot.", + "add": false, + "set": true, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "docDefaults.fontSize": { + "type": "font-size", + "description": "default font size.", + "add": false, + "set": true, + "get": true, + "readback": "Npt", + "enforcement": "report" + }, + "docDefaults.color": { + "type": "color", + "description": "default text color.", + "add": false, + "set": true, + "get": true, + "readback": "#RRGGBB", + "enforcement": "report" + }, + "docDefaults.bold": { + "type": "bool", + "description": "default bold flag.", + "add": false, + "set": true, + "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "docDefaults.italic": { + "type": "bool", + "description": "default italic flag.", + "add": false, + "set": true, + "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "docDefaults.rtl": { + "type": "bool", + "description": "default right-to-left flag.", + "add": false, + "set": true, + "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "docDefaults.alignment": { + "type": "string", + "description": "default paragraph alignment.", + "add": false, + "set": true, + "get": true, + "readback": "alignment token", + "enforcement": "report" + }, + "docDefaults.spaceBefore": { + "type": "string", + "description": "default paragraph space-before.", + "add": false, + "set": true, + "get": true, + "readback": "Npt", + "enforcement": "report" + }, + "docDefaults.spaceAfter": { + "type": "string", + "description": "default paragraph space-after.", + "add": false, + "set": true, + "get": true, + "readback": "Npt", + "enforcement": "report" + }, + "docDefaults.lineSpacing": { + "type": "string", + "description": "default paragraph line spacing.", + "add": false, + "set": true, + "get": true, + "readback": "1.5x or Npt", + "enforcement": "report" + }, + "autoSpaceDE": { + "type": "bool", + "description": "auto-spacing between East Asian and Latin text.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "autoSpaceDN": { + "type": "bool", + "description": "auto-spacing between East Asian and numeric text.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "kinsoku": { + "type": "bool", + "description": "Japanese kinsoku line breaking rules.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "overflowPunct": { + "type": "bool", + "description": "allow punctuation to overflow margin.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "embedFonts": { + "type": "bool", + "description": "embed TrueType fonts.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "embedSystemFonts": { + "type": "bool", + "description": "embed system fonts.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "saveSubsetFonts": { + "type": "bool", + "description": "save font subsets.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "mirrorMargins": { + "type": "bool", + "description": "mirror margins for facing pages.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "gutterAtTop": { + "type": "bool", + "description": "gutter at top.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "bookFoldPrinting": { + "type": "bool", + "description": "book fold printing layout.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "evenAndOddHeaders": { + "type": "bool", + "description": "different headers for even/odd pages.", + "add": false, + "set": true, + "get": true, + "readback": "true when settings/evenAndOddHeaders is present", + "enforcement": "report" + }, + "autoHyphenation": { + "type": "bool", + "description": "enable automatic hyphenation (settings/autoHyphenation).", + "add": false, + "set": true, + "get": true, + "readback": "true when settings/autoHyphenation is present", + "enforcement": "report" + }, + "defaultTabStop": { + "type": "string", + "description": "default tab stop (e.g. \"720\" twips or \"0.5in\").", + "add": false, + "set": true, + "get": false, + "examples": [ + "--prop defaultTabStop=720", + "--prop defaultTabStop=0.5in" + ], + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "displayBackgroundShape": { + "type": "bool", + "description": "display background shape.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "removePersonalInformation": { + "type": "bool", + "description": "remove personal info on save.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "removeDateAndTime": { + "type": "bool", + "description": "remove date/time on save.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "printFormsData": { + "type": "bool", + "description": "print only form data.", + "add": false, + "set": true, + "get": false, + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "pageWidth": { + "type": "length", + "add": false, + "set": true, + "get": true, + "description": "convenience readback from sectPr; primary edit path is /section[N].", + "readback": "length in cm (e.g. \"21cm\")", + "examples": [ + "--prop pageWidth=21cm" + ], + "enforcement": "report" + }, + "pageHeight": { + "type": "length", + "add": false, + "set": true, + "get": true, + "description": "convenience readback from sectPr; primary edit path is /section[N].", + "readback": "length in cm (e.g. \"29.7cm\")", + "examples": [ + "--prop pageHeight=29.7cm" + ], + "enforcement": "report" + }, + "orientation": { + "type": "enum", + "values": [ + "portrait", + "landscape" + ], + "add": false, + "set": true, + "get": true, + "description": "convenience readback from sectPr; primary edit path is /section[N].", + "readback": "orientation token", + "examples": [ + "--prop orientation=landscape" + ], + "enforcement": "report" + }, + "marginTop": { + "type": "length", + "add": false, + "set": true, + "get": true, + "description": "convenience readback from sectPr; primary edit path is /section[N].", + "readback": "length in cm", + "examples": [ + "--prop marginTop=2.54cm" + ], + "enforcement": "report" + }, + "marginBottom": { + "type": "length", + "add": false, + "set": true, + "get": true, + "description": "convenience readback from sectPr; primary edit path is /section[N].", + "readback": "length in cm", + "examples": [ + "--prop marginBottom=2.54cm" + ], + "enforcement": "report" + }, + "marginLeft": { + "type": "length", + "add": false, + "set": true, + "get": true, + "description": "convenience readback from sectPr; primary edit path is /section[N].", + "readback": "length in cm", + "examples": [ + "--prop marginLeft=3.18cm" + ], + "enforcement": "report" + }, + "marginRight": { + "type": "length", + "add": false, + "set": true, + "get": true, + "description": "convenience readback from sectPr; primary edit path is /section[N].", + "readback": "length in cm", + "examples": [ + "--prop marginRight=3.18cm" + ], + "enforcement": "report" + }, + "extended.application": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "from docProps/app.xml application identifier (e.g. \"Microsoft Word\").", + "readback": "application string", + "enforcement": "report" + }, + "bookFoldPrintingSheets": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "settings.xml bookFoldPrintingSheets — sheets per booklet signature when book-fold printing.", + "readback": "integer sheets-per-signature", + "enforcement": "report" + }, + "bookFoldReversePrinting": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "settings.xml bookFoldRevPrinting flag — true when book-fold printing reverses the page order.", + "readback": "true|false", + "enforcement": "report" + }, + "doNotDisplayPageBoundaries": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "settings.xml doNotDisplayPageBoundaries flag — Word view hides the page-boundary frame.", + "readback": "true when set", + "enforcement": "report" + }, + "columns.equalWidth": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "document-default columns equal-width flag (sectPr cols @equalWidth on the body sectPr).", + "readback": "true|false", + "enforcement": "report" + }, + "columns.separator": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "document-default columns separator flag (sectPr cols @sep on the body sectPr).", + "readback": "true|false", + "enforcement": "report" + }, + "locale": { + "type": "string", + "description": "primary document locale derived from theme themeFontLang (e.g. 'en-US', 'zh-CN', 'ar-SA'). Read-only summary — change via run lang.* slots or theme themeFontLang.", + "add": false, + "set": false, + "get": true, + "readback": "BCP-47 locale string", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/endnote.json b/schemas/help/docx/endnote.json new file mode 100644 index 000000000..94b0913ab --- /dev/null +++ b/schemas/help/docx/endnote.json @@ -0,0 +1,168 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "endnote", + "parent": "paragraph|body", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/endnote[N]" + ] + }, + "note": "Endnotes live in EndnotesPart. Semantics mirror footnote.json. Parent must be a paragraph path (/body/p[N]); use --index N to control position within the paragraph. Run-level parent (/body/p[N]/r[M]) is not accepted -- the endnote reference is inserted as a new run.", + "properties": { + "text": { + "type": "string", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop text=\"End-of-doc reference\"" + ], + "readback": "concatenated text", + "enforcement": "report" + }, + "direction": { + "type": "enum", + "values": [ + "rtl", + "ltr" + ], + "aliases": [ + "dir", + "bidi" + ], + "description": "Reading direction. 'rtl' writes on the endnote content paragraph and cascades to the paragraph mark.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop direction=rtl" + ], + "enforcement": "report" + }, + "align": { + "type": "enum", + "values": [ + "left", + "center", + "right", + "justify", + "both", + "distribute" + ], + "description": "Horizontal alignment applied to the endnote content paragraph ().", + "aliases": [ + "alignment" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop align=right" + ], + "enforcement": "report" + }, + "font.cs": { + "type": "string", + "description": "Complex-script font (rFonts/cs).", + "aliases": [ + "font.complexscript", + "font.complex" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop font.cs=\"Arabic Typesetting\"" + ], + "enforcement": "report" + }, + "font.ea": { + "type": "string", + "description": "East-Asian font slot (rFonts/eastAsia) — Chinese / Japanese / Korean typefaces.", + "aliases": [ + "font.eastasia", + "font.eastasian" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop font.ea=\"メイリオ\"" + ], + "enforcement": "report" + }, + "font.latin": { + "type": "string", + "description": "Latin font slots (rFonts/ascii + hAnsi) — ASCII / Western text.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop font.latin=Calibri" + ], + "enforcement": "report" + }, + "bold.cs": { + "type": "bool", + "description": "complex-script bold for the endnote's runs (). Required for Arabic / Hebrew bold rendering.", + "aliases": [ + "font.bold.cs", + "boldcs" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop bold.cs=true" + ], + "enforcement": "report" + }, + "italic.cs": { + "type": "bool", + "description": "complex-script italic () for the endnote's runs.", + "aliases": [ + "font.italic.cs", + "italiccs" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop italic.cs=true" + ], + "enforcement": "report" + }, + "size.cs": { + "type": "font-size", + "description": "complex-script font size () for the endnote's runs.", + "aliases": [ + "font.size.cs", + "sizecs" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop size.cs=14pt" + ], + "enforcement": "report" + }, + "id": { + "type": "number", + "description": "OOXML endnote id; source of @endnoteId in stable path /endnote[@endnoteId=N].", + "add": false, + "set": false, + "get": true, + "readback": "integer", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/equation.json b/schemas/help/docx/equation.json new file mode 100644 index 000000000..4fc59b294 --- /dev/null +++ b/schemas/help/docx/equation.json @@ -0,0 +1,38 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "equation", + "parent": "body|paragraph", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/body/oMathPara[N]", + "/body/p[N]/oMath[M]" + ] + }, + "note": "Aliases: formula, math. formula input is parsed by FormulaParser (LaTeX-ish). Display mode wraps in oMathPara; inline mode appends oMath to the parent paragraph. Get returns only `mode` (display|inline) in Format[]; the formula source itself is in DocumentNode.Text.", + "extends": "_shared/equation", + "properties": { + "mode": { + "type": "enum", + "values": [ + "display", + "inline" + ], + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop mode=inline" + ], + "readback": "display | inline", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/field.json b/schemas/help/docx/field.json new file mode 100644 index 000000000..f6ab5c557 --- /dev/null +++ b/schemas/help/docx/field.json @@ -0,0 +1,105 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "field", + "parent": "paragraph|body", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/field[N]"] + }, + "note": "Complex field (fldChar: begin/instr/separate/result/end), addressed as a whole at /field[N]; /body/p[N]/r[M] returns the inner fieldChar run, not the field. Per-type required props: mergefield/ref → name (aka fieldName/bookmarkName); seq → identifier; styleref → styleName; docproperty → propertyName; if → expression (+ optional trueText/falseText); date/time accept optional format.", + "properties": { + "evaluated": { + "type": "bool", + "description": "cross-handler protocol flag — true when the field has a cached result run that the opening application can display, false when the field was written without a cached value (view text substitutes #OCLI_NOTEVAL!{instr}; view issues emits subtype field_not_evaluated). For dirty cached fields the cache is still trusted by evaluated; staleness surfaces separately via subtype field_cache_stale. Read this via get --json instead of pattern-matching the sentinel, since multiple fields per paragraph and literal user content may collide.", + "add": false, "set": false, "get": true, + "readback": "true|false (true ⇒ cached result run present)", + "enforcement": "report" + }, + "fieldType": { + "type": "enum", + "values": ["page", "pagenum", "pagenumber", "numpages", "date", "author", "title", "time", "filename", "section", "sectionpages", "mergefield", "ref", "pageref", "noteref", "seq", "styleref", "docproperty", "if", "createdate", "savedate", "printdate", "edittime", "lastsavedby", "subject", "numwords", "numchars", "revnum", "template", "comments", "doccomments", "keywords"], + "aliases": ["fieldtype", "type"], + "add": true, "set": true, "get": true, + "examples": ["--prop fieldType=page"], + "readback": "resolved instruction", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "Per-type identifier: mergefield → field name (e.g. CustomerName); ref/pageref/noteref → target bookmark name; styleref → style name; docproperty → property name. Aliases preserve historical naming differences (e.g. ref docs called this 'bookmarkName').", + "aliases": ["fieldName", "fieldname", "bookmarkName", "bookmarkname", "bookmark", "styleName", "stylename", "propertyName", "propertyname"], + "add": true, "set": true, "get": true, + "examples": ["--prop name=CustomerName", "--prop bookmarkName=Section1", "--prop styleName=\"Heading 1\""], + "readback": "n/a (embedded in instruction)", + "enforcement": "report" + }, + "id": { + "type": "string", + "description": "SEQ field's identifier (sequence label). Defaults to 'name' when 'id' is not supplied. Alias: identifier.", + "aliases": ["identifier"], + "add": true, "set": true, "get": true, + "examples": ["--prop id=Figure", "--prop identifier=Figure"], + "readback": "n/a (embedded in instruction)", + "enforcement": "report" + }, + "expression": { + "type": "string", + "description": "IF field's logical expression (e.g. 'MERGEFIELD Gender = \"Male\"'). Add/Set only — surfaces back inside the `instruction` readback, not as its own Format key.", + "aliases": ["condition"], + "add": true, "set": true, "get": false, + "examples": ["--prop expression='{ MERGEFIELD Gender } = \"Male\"'"], + "readback": "n/a (embedded in instruction)", + "enforcement": "report" + }, + "trueText": { + "type": "string", + "description": "IF field's text shown when expression evaluates true. Add/Set only — surfaces back inside the `instruction` readback.", + "aliases": ["truetext"], + "add": true, "set": true, "get": false, + "examples": ["--prop trueText=\"Mr.\""], + "readback": "n/a (embedded in instruction)", + "enforcement": "report" + }, + "falseText": { + "type": "string", + "description": "IF field's text shown when expression evaluates false. Add/Set only — surfaces back inside the `instruction` readback.", + "aliases": ["falsetext"], + "add": true, "set": true, "get": false, + "examples": ["--prop falseText=\"Ms.\""], + "readback": "n/a (embedded in instruction)", + "enforcement": "report" + }, + "hyperlink": { + "type": "bool", + "description": "REF field: append \\h switch so the inserted reference becomes a clickable hyperlink to the bookmark target. Add/Set only — surfaces back as a switch inside the `instruction` readback.", + "add": true, "set": true, "get": false, + "examples": ["--prop hyperlink=true"], + "readback": "n/a (embedded in instruction)", + "enforcement": "report" + }, + "format": { + "type": "string", + "description": "switch-style format (e.g. '\\@ \"yyyy-MM-dd\"' for date).", + "add": true, "set": true, "get": true, + "examples": ["--prop format=\"yyyy-MM-dd\""], + "readback": "instruction switches", + "enforcement": "report" + }, + "instruction": { + "type": "string", + "description": "Raw field instruction text. Bypasses fieldType-specific helpers — useful for arbitrary fields not covered by the typed shortcuts.", + "aliases": ["instr", "code"], + "add": true, "set": true, "get": true, + "examples": ["--prop instruction=' DATE \\@ \"yyyy年MM月\" '"], + "readback": "instrText element text content", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/fieldchar.json b/schemas/help/docx/fieldchar.json new file mode 100644 index 000000000..12bab673a --- /dev/null +++ b/schemas/help/docx/fieldchar.json @@ -0,0 +1,29 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "fieldChar", + "parent": "paragraph", + "operations": { + "add": false, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/body/p[@paraId=X]/r[N]", "/header[N]/p[M]/r[K]", "/footer[N]/p[M]/r[K]"] + }, + "note": "Field-character marker (w:fldChar) — inline atom that delimits a complex field's begin / separate / end boundaries. Atomic add is intentionally NOT supported because a fldChar in isolation is invalid OOXML; use --type field to insert a complete begin+instrText+separate+cached+end sequence as one unit. Get/Set on individual fldChars allows audit→fix workflows to inspect and adjust an existing field's structure.", + "properties": { + "fieldCharType": { + "type": "enum", + "values": ["begin", "separate", "end"], + "aliases": ["fieldchartype"], + "add": false, "set": true, "get": true, + "required": true, + "examples": ["--prop fieldCharType=separate"], + "readback": "fldChar fldCharType attribute", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/footer.json b/schemas/help/docx/footer.json new file mode 100644 index 000000000..d811cf485 --- /dev/null +++ b/schemas/help/docx/footer.json @@ -0,0 +1,181 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "footer", + "parent": "/", + "addParent": "/", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/footer[N]" + ] + }, + "note": "Mirror of header.json — same surface, stored in FooterParts. Duplicate type per section rejected at Add. A single Add supports at most one text + one field pair. For composite footers like 'Page X of Y' (two fields + literal text), create the footer first, then Add additional runs/fields to its paragraph (/footer[N]/p[1]) one by one — see examples.", + "examples": [ + "Simple page-number footer: officecli add file.docx / --type footer --prop field=page --prop align=center", + "'Page X of Y' — must be built in steps after creating the footer:", + " 1) officecli add file.docx / --type footer --prop text=\"Page \" --prop align=center", + " 2) officecli add file.docx \"/footer[1]/p[1]\" --type field --prop fieldType=page", + " 3) officecli add file.docx \"/footer[1]/p[1]\" --type run --prop text=\" of \"", + " 4) officecli add file.docx \"/footer[1]/p[1]\" --type field --prop fieldType=numpages" + ], + "properties": { + "type": { + "type": "enum", + "values": [ + "default", + "first", + "even" + ], + "aliases": [ + "kind", + "ref" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop type=default" + ], + "readback": "innerText of HeaderFooterValues", + "enforcement": "strict" + }, + "text": { + "type": "string", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop text=\"My Footer\"" + ], + "readback": "concatenated Text.Descendants", + "enforcement": "strict" + }, + "align": { + "type": "enum", + "values": [ + "left", + "center", + "right", + "justify", + "both", + "distribute" + ], + "aliases": [ + "alignment" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop align=center" + ], + "readback": "first-paragraph Justification.Val.InnerText", + "enforcement": "strict" + }, + "direction": { + "type": "enum", + "values": [ + "rtl", + "ltr" + ], + "aliases": [ + "dir", + "bidi" + ], + "description": "Reading direction. 'rtl' writes on the footer paragraph, on the paragraph mark, and on every run (text + field runs alike) so Arabic / Hebrew character order reverses end-to-end. 'ltr' clears all three.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop direction=rtl" + ], + "enforcement": "strict" + }, + "font": { + "type": "string", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font=\"Arial\"" + ], + "readback": "Ascii or HighAnsi font name", + "enforcement": "strict" + }, + "size": { + "type": "font-size", + "description": "font size. Accepts bare number or pt-suffixed.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop size=12" + ], + "readback": "unit-qualified pt", + "enforcement": "strict" + }, + "bold": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop bold=true" + ], + "readback": "true when bold, key absent otherwise", + "enforcement": "strict" + }, + "italic": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop italic=true" + ], + "readback": "true when italic, key absent otherwise", + "enforcement": "strict" + }, + "color": { + "type": "color", + "description": "font color. Accepts #RRGGBB, RRGGBB, named colors (red, blue…), rgb(r,g,b), or 3-char shorthand (F00).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop color=#FF0000" + ], + "readback": "#-prefixed uppercase hex", + "enforcement": "strict" + }, + "field": { + "type": "enum", + "values": [ + "page", + "pagenum", + "pagenumber", + "numpages", + "date", + "author", + "title", + "time", + "filename" + ], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop field=page" + ], + "readback": "not surfaced as a distinct key", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/docx/footnote.json b/schemas/help/docx/footnote.json new file mode 100644 index 000000000..8310feca3 --- /dev/null +++ b/schemas/help/docx/footnote.json @@ -0,0 +1,173 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "footnote", + "parent": "paragraph|body", + "addParent": "/body/p[N]", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": [ + "/footnote[@footnoteId=N]" + ], + "positional": [ + "/footnotes/footnote[N]" + ] + }, + "note": "Footnotes live in FootnotesPart. A FootnoteReference run is inserted at the anchor point; the note body is appended to footnotes.xml. Parent must be a paragraph path (/body/p[N]); use --index N to control position within the paragraph. Run-level parent (/body/p[N]/r[M]) is not accepted -- the footnote reference is inserted as a new run.", + "properties": { + "text": { + "type": "string", + "description": "footnote text. Required.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop text=\"See ref [1]\"" + ], + "readback": "concatenated text", + "enforcement": "report" + }, + "direction": { + "type": "enum", + "values": [ + "rtl", + "ltr" + ], + "aliases": [ + "dir", + "bidi" + ], + "description": "Reading direction. 'rtl' writes on the footnote content paragraph and cascades to the paragraph mark.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop direction=rtl" + ], + "enforcement": "report" + }, + "align": { + "type": "enum", + "values": [ + "left", + "center", + "right", + "justify", + "both", + "distribute" + ], + "description": "Horizontal alignment applied to the footnote content paragraph ().", + "aliases": [ + "alignment" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop align=right" + ], + "enforcement": "report" + }, + "font.cs": { + "type": "string", + "description": "Complex-script font (rFonts/cs) — Arabic / Hebrew typeface.", + "aliases": [ + "font.complexscript", + "font.complex" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop font.cs=\"Arabic Typesetting\"" + ], + "enforcement": "report" + }, + "font.ea": { + "type": "string", + "description": "East-Asian font (rFonts/eastAsia).", + "aliases": [ + "font.eastasia", + "font.eastasian" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop font.ea=\"メイリオ\"" + ], + "enforcement": "report" + }, + "font.latin": { + "type": "string", + "description": "Latin font slot (rFonts/ascii + hAnsi).", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop font.latin=Calibri" + ], + "enforcement": "report" + }, + "bold.cs": { + "type": "bool", + "description": "complex-script bold (). Required for Arabic / Hebrew bold rendering.", + "aliases": [ + "font.bold.cs", + "boldcs" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop bold.cs=true" + ], + "enforcement": "report" + }, + "italic.cs": { + "type": "bool", + "description": "complex-script italic () for the footnote's runs.", + "aliases": [ + "font.italic.cs", + "italiccs" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop italic.cs=true" + ], + "enforcement": "report" + }, + "size.cs": { + "type": "font-size", + "description": "complex-script font size () for the footnote's runs.", + "aliases": [ + "font.size.cs", + "sizecs" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop size.cs=14pt" + ], + "enforcement": "report" + }, + "id": { + "type": "number", + "description": "OOXML footnote id (w:footnote/@w:id). Assigned by the writer; surfaces only on Get/Query.", + "add": false, + "set": false, + "get": true, + "readback": "integer footnote ID", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/formfield.json b/schemas/help/docx/formfield.json new file mode 100644 index 000000000..98f027fc0 --- /dev/null +++ b/schemas/help/docx/formfield.json @@ -0,0 +1,59 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "formfield", + "parent": "paragraph|body", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": ["/formfield[@name=NAME]"], + "positional": ["/formfield[N]"] + }, + "note": "Form fields embed a BookmarkStart/End so names share the bookmark namespace and validation rules. Three kinds: text (default), checkbox, dropdown.", + "properties": { + "type": { + "type": "enum", + "description": "form field type.", + "values": ["text", "checkbox", "check", "dropdown"], + "aliases": ["formfieldtype"], + "add": true, "set": true, "get": true, + "examples": ["--prop type=text"], + "readback": "one of values", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "form field name (required for stable addressing). Same constraints as bookmark name.", + "add": true, "set": true, "get": true, + "examples": ["--prop name=email"], + "readback": "name as stored", + "enforcement": "strict" + }, + "text": { + "type": "string", + "description": "initial text value (text fields only). Alias: value.", + "aliases": ["value"], + "add": true, "set": true, "get": true, + "examples": ["--prop text=default"], + "readback": "initial text for text type", + "enforcement": "report" + }, + "checked": { + "type": "bool", + "description": "default state (checkbox only).", + "add": true, "set": true, "get": true, + "appliesWhen": { "type": ["checkbox", "check"] }, + "examples": ["--prop checked=true"], + "readback": "true/false", + "enforcement": "report" + }, + "default": { "type":"string", "add":false, "set":false, "get":true, "description":"form-field default value. Text-fields surface the default string; dropdowns surface the integer default index.", "readback":"default value (string or integer)", "enforcement":"report" }, + "enabled": { "type":"bool", "add":false, "set":false, "get":true, "description":"true when the form field accepts user input (FFData @enabled). Defaults true when the element is absent.", "readback":"true|false", "enforcement":"report" }, + "hasFormFieldData": { "type":"bool", "add":false, "set":false, "get":true, "description":"true when the run carries an embedded fldData payload (legacy form-field binary blob).", "readback":"true when present", "enforcement":"report" } + } +} diff --git a/schemas/help/docx/header.json b/schemas/help/docx/header.json new file mode 100644 index 000000000..bb712a853 --- /dev/null +++ b/schemas/help/docx/header.json @@ -0,0 +1,184 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "header", + "parent": "/", + "addParent": "/", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/header[N]" + ] + }, + "note": "Headers are stored in HeaderParts and referenced by the last sectPr. Duplicate type rejected at Add. 'first' type auto-enables TitlePage. Field insertion uses complex fldChar (begin/instr/separate/result/end). A single Add supports at most one text + one field pair; composite headers like 'Page X of Y' must be built in steps by adding additional runs/fields to the header's paragraph (/header[N]/p[1]) after creation — see examples.", + "examples": [ + "Simple page-number header: officecli add file.docx / --type header --prop field=page --prop align=right", + "'Page X of Y' — build in steps after creating the header:", + " 1) officecli add file.docx / --type header --prop text=\"Page \" --prop align=right", + " 2) officecli add file.docx \"/header[1]/p[1]\" --type field --prop fieldType=page", + " 3) officecli add file.docx \"/header[1]/p[1]\" --type run --prop text=\" of \"", + " 4) officecli add file.docx \"/header[1]/p[1]\" --type field --prop fieldType=numpages" + ], + "properties": { + "type": { + "type": "enum", + "description": "header scope.", + "values": [ + "default", + "first", + "even" + ], + "aliases": [ + "kind", + "ref" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop type=default" + ], + "readback": "innerText of HeaderFooterValues", + "enforcement": "strict" + }, + "text": { + "type": "string", + "description": "header text (single run).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop text=\"My Header\"" + ], + "readback": "concatenated Text.Descendants", + "enforcement": "strict" + }, + "align": { + "type": "enum", + "values": [ + "left", + "center", + "right", + "justify", + "both", + "distribute" + ], + "aliases": [ + "alignment" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop align=center" + ], + "readback": "first-paragraph Justification.Val.InnerText", + "enforcement": "strict" + }, + "direction": { + "type": "enum", + "values": [ + "rtl", + "ltr" + ], + "aliases": [ + "dir", + "bidi" + ], + "description": "Reading direction. 'rtl' writes on the header paragraph, on the paragraph mark, and on every run (text + field runs alike) so Arabic / Hebrew character order reverses end-to-end. 'ltr' clears all three.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop direction=rtl" + ], + "enforcement": "strict" + }, + "font": { + "type": "string", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font=\"Arial\"" + ], + "readback": "Ascii or HighAnsi font name", + "enforcement": "strict" + }, + "size": { + "type": "font-size", + "description": "font size. Accepts bare number or pt-suffixed.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop size=12" + ], + "readback": "unit-qualified pt (e.g. \"12pt\")", + "enforcement": "strict" + }, + "bold": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop bold=true" + ], + "readback": "true when bold, key absent otherwise", + "enforcement": "strict" + }, + "italic": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop italic=true" + ], + "readback": "true when italic, key absent otherwise", + "enforcement": "strict" + }, + "color": { + "type": "color", + "description": "font color. Accepts #RRGGBB, RRGGBB, named colors (red, blue…), rgb(r,g,b), or 3-char shorthand (F00).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop color=#FF0000" + ], + "readback": "#-prefixed uppercase hex", + "enforcement": "strict" + }, + "field": { + "type": "enum", + "description": "complex field to insert (page/numpages/date/author/title/time/filename, or an arbitrary field name).", + "values": [ + "page", + "pagenum", + "pagenumber", + "numpages", + "date", + "author", + "title", + "time", + "filename" + ], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop field=page" + ], + "readback": "not surfaced as a distinct key", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/docx/hyperlink.json b/schemas/help/docx/hyperlink.json new file mode 100644 index 000000000..4dd97dc63 --- /dev/null +++ b/schemas/help/docx/hyperlink.json @@ -0,0 +1,147 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "hyperlink", + "parent": "paragraph", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/body/p[N]/hyperlink[M]" + ] + }, + "note": "Aliases: link. Exactly one of 'url' (external) or 'anchor' (internal bookmark ref) is required. Colors default to theme Hyperlink (or 0563C1 fallback) with single underline.", + "extends": "_shared/hyperlink", + "properties": { + "url": { + "type": "string", + "description": "external URL. Aliases: href, link. docx Set accepts all three (url canonical).", + "aliases": [ + "href", + "link" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop url=https://example.com" + ], + "readback": "URL string", + "enforcement": "report" + }, + "anchor": { + "type": "string", + "description": "bookmark name for internal links. Set after Add not supported — formatting lives on inner runs (Set the run, not the hyperlink wrapper).", + "aliases": [ + "bookmark" + ], + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop anchor=section1" + ], + "readback": "anchor name for internal links", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "display text. Defaults to url or anchor value if omitted.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop text=\"click here\"" + ], + "readback": "concatenated run text", + "enforcement": "report" + }, + "rStyle": { + "type": "string", + "description": "Run style id applied to the hyperlink's run (typically 'Hyperlink' to inherit theme color/underline).", + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop rStyle=Hyperlink" + ], + "readback": "style id", + "enforcement": "report" + }, + "color": { + "type": "color", + "description": "override link color (default: theme Hyperlink or 0563C1). Set after Add not supported — formatting lives on inner runs (Set the run, not the hyperlink wrapper).", + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop color=#0000FF" + ], + "readback": "#-prefixed uppercase hex, or scheme color name (e.g. 'hyperlink', 'followedhyperlink') when using theme default", + "enforcement": "report" + }, + "font": { + "type": "string", + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop font=\"Calibri\"" + ], + "readback": "font name", + "enforcement": "report", + "description": " Set after Add not supported — formatting lives on inner runs (Set the run, not the hyperlink wrapper)." + }, + "size": { + "type": "length", + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop size=11" + ], + "readback": "unit-qualified pt", + "enforcement": "report", + "description": " Set after Add not supported — formatting lives on inner runs (Set the run, not the hyperlink wrapper)." + }, + "bold": { + "type": "bool", + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop bold=true" + ], + "readback": "true/false", + "enforcement": "report", + "description": " Set after Add not supported — formatting lives on inner runs (Set the run, not the hyperlink wrapper)." + }, + "italic": { + "type": "bool", + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop italic=true" + ], + "readback": "true/false", + "enforcement": "report", + "description": " Set after Add not supported — formatting lives on inner runs (Set the run, not the hyperlink wrapper)." + }, + "docLocation": { + "type": "string", + "description": "w:docLocation — frame name (or location-within-document for non-text-anchor refs). Preserved on dump round-trip. Get only; set via the underlying hyperlink rewrite path on Add.", + "aliases": ["doclocation"], + "add": false, + "set": false, + "get": true, + "readback": "doc location string", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/instrtext.json b/schemas/help/docx/instrtext.json new file mode 100644 index 000000000..d20ee8ea0 --- /dev/null +++ b/schemas/help/docx/instrtext.json @@ -0,0 +1,29 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "instrText", + "parent": "paragraph", + "operations": { + "add": false, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/body/p[@paraId=X]/r[N]", "/header[N]/p[M]/r[K]", "/footer[N]/p[M]/r[K]"] + }, + "note": "Field-instruction text (w:instrText) — the body of a complex field that holds the instruction string (e.g. 'PAGE \\\\* MERGEFORMAT', 'DATE \\\\@ \"yyyy-MM-dd\"'). Atomic add is intentionally NOT supported because instrText outside a field is invalid; use --type field to insert a complete sequence. Set is supported so audit→fix workflows can rewrite a field's instruction (e.g. PAGE → DATE) without touching the surrounding fldChar markers.", + "properties": { + "instruction": { + "type": "string", + "description": "The Word field instruction. Leading/trailing spaces inside the value are significant — they form the OOXML separator between switches. Alias: instr.", + "aliases": ["instr"], + "add": false, "set": true, "get": true, + "required": true, + "examples": ["--prop 'instruction= PAGE \\\\* MERGEFORMAT '", "--prop 'instr= DATE \\\\@ \"yyyy-MM-dd\" '"], + "readback": "instrText element text content", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/level.json b/schemas/help/docx/level.json new file mode 100644 index 000000000..39082b3f3 --- /dev/null +++ b/schemas/help/docx/level.json @@ -0,0 +1,193 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "level", + "parent": "abstractNum", + "addParent": "/numbering/abstractNum[@id=N]", + "operations": { + "add": true, + "set": true, + "get": true, + "query": false, + "remove": true + }, + "paths": { + "positional": ["/numbering/abstractNum[@id=N]/level[L]"] + }, + "note": "Single under an . L is the level index (w:ilvl, 0..8). 'add abstractNum' pre-populates 9 default levels (lvl%3 cycle); adding another --type level with the same ilvl replaces it in place. Aliases lvl / level. Top-level abstractNum.* properties also accept level0.* dotted forms for level-0 customization at creation time — this element is for adjusting levels 1..8 or replacing levels post-Add.", + "properties": { + "ilvl": { + "type": "int", + "description": "level index 0..8 (w:ilvl). Required on Add.", + "add": true, + "set": false, + "get": true, + "examples": ["--prop ilvl=1"], + "readback": "integer 0..8", + "enforcement": "strict" + }, + "format": { + "type": "string", + "description": "numFmt for this level. Common values: decimal, decimalZero, upperRoman, lowerRoman, upperLetter, lowerLetter, ordinal, cardinalText, ordinalText, bullet, chineseCounting, chineseLegalSimplified, japaneseCounting, koreanCounting, hex. Default decimal.", + "aliases": ["fmt", "numFmt"], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop format=decimal", + "--prop format=chineseCounting" + ], + "readback": "numFmt token (lowerCamel as in OOXML)", + "enforcement": "report" + }, + "lvlText": { + "type": "string", + "description": "lvlText template. Use %N to insert level N's running counter (e.g. '%1.', '%1.%2', '第%1章'). Default: %{ilvl+1}. for ordered, • for bullet.", + "aliases": ["text"], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lvlText=\"%1.\"", + "--prop lvlText=\"第%1章\"" + ], + "readback": "lvlText string (literal, including %N placeholders)", + "enforcement": "report" + }, + "start": { + "type": "int", + "description": "starting number. Default 1.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop start=5"], + "readback": "integer", + "enforcement": "report" + }, + "indent": { + "type": "int", + "description": "left indent in twips (pPr/ind/left). On Add, default (ilvl+1)*720.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop indent=720"], + "readback": "integer twips", + "enforcement": "report" + }, + "hanging": { + "type": "int", + "description": "hanging indent in twips (pPr/ind/hanging). Default 360.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop hanging=360"], + "readback": "integer twips", + "enforcement": "report" + }, + "justification": { + "type": "enum", + "values": ["left", "center", "right"], + "aliases": ["jc"], + "description": "lvlJc — marker alignment within the indent area. Default left.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop justification=left"], + "readback": "left | center | right", + "enforcement": "report" + }, + "suff": { + "type": "enum", + "values": ["tab", "space", "nothing"], + "description": "separator between the marker and the text content. Default tab.", + "add": true, + "set": true, + "get": false, + "examples": ["--prop suff=space"], + "readback": "n/a", + "enforcement": "report" + }, + "lvlRestart": { + "type": "int", + "description": "w:lvlRestart — level at which to restart this counter (0 = never restart).", + "add": true, + "set": true, + "get": false, + "examples": ["--prop lvlRestart=0"], + "readback": "n/a", + "enforcement": "report" + }, + "isLgl": { + "type": "bool", + "description": "w:isLgl — render this level's counter as decimal regardless of numFmt (legal-style numbering).", + "add": true, + "set": true, + "get": false, + "examples": ["--prop isLgl=true"], + "readback": "n/a", + "enforcement": "report" + }, + "direction": { + "type": "enum", + "values": ["rtl", "ltr"], + "aliases": ["dir", "bidi"], + "description": "right-to-left direction on the level's pPr. Only 'rtl' writes ; 'ltr' is the canonical clear.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop direction=rtl"], + "readback": "n/a", + "enforcement": "report" + }, + "font": { + "type": "string", + "description": "marker font (rPr/rFonts). Useful for bullet glyphs that need Symbol or Wingdings.", + "add": true, + "set": true, + "get": false, + "examples": ["--prop font=Symbol"], + "readback": "n/a", + "enforcement": "report" + }, + "size": { + "type": "font-size", + "description": "marker font size (rPr/sz). Bare number = pt.", + "add": true, + "set": true, + "get": false, + "examples": ["--prop size=14"], + "readback": "n/a", + "enforcement": "report" + }, + "color": { + "type": "color", + "description": "marker color (rPr/color).", + "add": true, + "set": true, + "get": false, + "examples": ["--prop color=#FF0000"], + "readback": "n/a", + "enforcement": "report" + }, + "bold": { + "type": "bool", + "description": "bold marker (rPr/b).", + "add": true, + "set": true, + "get": false, + "examples": ["--prop bold=true"], + "readback": "n/a", + "enforcement": "report" + }, + "italic": { + "type": "bool", + "description": "italic marker (rPr/i).", + "add": true, + "set": true, + "get": false, + "examples": ["--prop italic=true"], + "readback": "n/a", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/num.json b/schemas/help/docx/num.json new file mode 100644 index 000000000..efaf17a09 --- /dev/null +++ b/schemas/help/docx/num.json @@ -0,0 +1,128 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "num", + "parent": "numbering", + "addParent": "/numbering", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": ["/numbering/num[@id=N]"], + "positional": ["/numbering/num[N]"] + }, + "note": "Numbering instance (). A thin pointer that references one via abstractNumId — the abstractNum is the template, the num is the live counter. Paragraphs reference 'numId' (this element's id), not abstractNumId. Two modes: (B/C) --prop abstractNumId=N reuses an existing template; (A) --prop format=... auto-creates a matching abstractNum on the fly. By default each new num gets a startOverride on level 0 so two instances of the same abstractNum count independently — pass --prop continue=true to inherit Word's literal counter-continuation behavior instead.", + "properties": { + "id": { + "type": "int", + "description": "numId (w:numId, unique within /numbering). Omit for auto-assignment. Paragraphs reference this id via --prop numId=N.", + "add": true, + "set": false, + "get": true, + "examples": ["--prop id=3"], + "readback": "integer numId", + "enforcement": "strict" + }, + "abstractNumId": { + "type": "int", + "description": "abstractNum to reuse (mode B/C). Must already exist in /numbering. Mutually exclusive with mode-A format-based auto-create. Set to mutate after creation.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop abstractNumId=0"], + "readback": "integer abstractNumId", + "enforcement": "strict" + }, + "format": { + "type": "string", + "description": "mode A: when no abstractNumId is supplied, build a matching abstractNum from these props (format/text/indent/type/start) and bind this num to it. Same vocabulary as abstractNum.format.", + "aliases": ["fmt", "numFmt"], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop format=decimal", + "--prop format=bullet" + ], + "readback": "n/a", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "mode A: lvlText for the auto-created abstractNum level 0.", + "aliases": ["lvlText"], + "add": true, + "set": false, + "get": false, + "examples": ["--prop text=\"%1.\""], + "readback": "n/a", + "enforcement": "report" + }, + "indent": { + "type": "int", + "description": "mode A: left indent (twips) for the auto-created abstractNum level 0.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop indent=720"], + "readback": "n/a", + "enforcement": "report" + }, + "type": { + "type": "enum", + "values": ["hybridMultilevel", "multilevel", "singleLevel"], + "description": "mode A: multiLevelType for the auto-created abstractNum.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop type=hybridMultilevel"], + "readback": "n/a", + "enforcement": "report" + }, + "start": { + "type": "int", + "description": "starting number for level 0. Shorthand for startOverride.0. Default behavior: a fresh num auto-injects a startOverride on level 0 (using the abstractNum's level-0 start, typically 1) so independent instances of the same template don't share counters. Pass --prop continue=true to suppress this.", + "add": true, + "set": false, + "get": false, + "examples": ["--prop start=5"], + "readback": "n/a", + "enforcement": "report" + }, + "startOverride.": { + "type": "int", + "description": "per-level startOverride (N = 0..8). Emits . Use to reset specific levels without disturbing others.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop startOverride.0=1 --prop startOverride.1=1" + ], + "readback": "n/a", + "enforcement": "report" + }, + "continue": { + "type": "bool", + "description": "if true, suppress the default level-0 startOverride injection — this num inherits Word's literal counter-continuation behavior from the abstractNum (two num instances on the same abstractNum keep counting together).", + "add": true, + "set": false, + "get": false, + "examples": ["--prop continue=true"], + "readback": "n/a", + "enforcement": "report" + }, + "numId": { + "type": "int", + "description": "readback alias for id (matches the w:numId attribute name).", + "add": false, + "set": false, + "get": true, + "readback": "integer numId", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/numbering.json b/schemas/help/docx/numbering.json new file mode 100644 index 000000000..46044ab3f --- /dev/null +++ b/schemas/help/docx/numbering.json @@ -0,0 +1,26 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "numbering", + "parent": "document", + "container": true, + "operations": { + "add": false, + "set": false, + "get": true, + "query": true, + "remove": false + }, + "paths": { + "positional": ["/numbering"] + }, + "note": "NumberingDefinitionsPart container — bullet / numbered list definitions. The container itself is read-only; mutate via its children: --type abstractNum (the template) and --type num (the instance), see docx/abstractNum.json and docx/num.json. Paragraphs/styles then bind to an instance by id via the 'numId' / 'ilvl' props, or via the higher-level 'listStyle' shortcut (see docx/paragraph.json).", + "properties": { + "abstractNumCount": { "type":"number", "add":false, "set":false, "get":true, "description":"total number of abstractNum definitions in numbering.xml.", "readback":"integer abstractNum count", "enforcement":"report" }, + "abstractNumId": { "type":"number", "add":false, "set":false, "get":true, "description":"per-num child readback — the abstractNumId referenced by each /num child. Surfaces on enumerated num child nodes, not the numbering container itself.", "readback":"integer abstractNum reference", "enforcement":"report" } + }, + "children": [ + { "element": "abstractNum", "pathSegment": "abstractNum", "cardinality": "0..n" }, + { "element": "num", "pathSegment": "num", "cardinality": "0..n" } + ] +} diff --git a/schemas/help/docx/ole.json b/schemas/help/docx/ole.json new file mode 100644 index 000000000..48fe6e80d --- /dev/null +++ b/schemas/help/docx/ole.json @@ -0,0 +1,25 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "ole", + "parent": "paragraph|body", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/body/p[N]/ole[M]", + "/header[N]/ole[M]", + "/footer[N]/ole[M]" + ] + }, + "note": "Aliases: oleobject, object, embed. Embeds a binary package plus a preview image. Source accepted as file path, URL, or data-URI.", + "extends": [ + "_shared/ole", + "_shared/ole.docx-pptx" + ] +} diff --git a/schemas/help/docx/pagebreak.json b/schemas/help/docx/pagebreak.json new file mode 100644 index 000000000..cfa8fdfa9 --- /dev/null +++ b/schemas/help/docx/pagebreak.json @@ -0,0 +1,39 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "pagebreak", + "parent": "paragraph|body", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/body/p[@paraId=X]/r[N]", "/header[N]/p[M]/r[K]", "/footer[N]/p[M]/r[K]"] + }, + "note": "Inline w:br element wrapped in a run. Aliases: columnbreak, break. type=column → column break; type=page (default) → page break; type=line/textWrapping → soft line break. Surfaced by Get as type=break (when the run carries no alongside the ).", + "properties": { + "type": { + "type": "enum", + "values": ["page", "column", "textWrapping", "line"], + "default": "page", + "add": true, "set": false, "get": true, + "examples": ["--prop type=page", "--prop type=column"], + "readback": "n/a (use 'breakType' on Get / Set)", + "enforcement": "report", + "description": "Add-only alias; Get surfaces this as 'breakType', and Set requires 'breakType'." + }, + "breakType": { + "type": "enum", + "values": ["page", "column", "textWrapping", "line"], + "aliases": ["breaktype"], + "add": false, "set": true, "get": true, + "examples": ["--prop breakType=column"], + "readback": "br type attribute", + "enforcement": "report", + "description": "Canonical key on Get/Set. Add accepts 'type' as a parallel synonym for symmetry with the historical alias." + } + } +} diff --git a/schemas/help/docx/paragraph.json b/schemas/help/docx/paragraph.json new file mode 100644 index 000000000..e932a317f --- /dev/null +++ b/schemas/help/docx/paragraph.json @@ -0,0 +1,1180 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "paragraph", + "elementAliases": ["p"], + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": [ + "/body/p[@paraId=ID]" + ], + "positional": [ + "/body/p[N]" + ] + }, + "note": "Get returns canonical keys: spaceBefore/spaceAfter/lineSpacing/align (legacy aliases spacebefore/linespacing/halign still accepted on Add/Set). effective.* keys are read-only values resolved through paragraph style → docDefaults from the first run; each carries an effective.X.src pointer to the writing layer (e.g. /styles/Heading1). Suppressed when the paragraph (or first run) sets the direct value.", + "children": [ + { + "element": "run", + "pathSegment": "r", + "cardinality": "0..n" + } + ], + "extends": "_shared/paragraph", + "properties": { + "align": { + "type": "enum", + "values": [ + "left", + "center", + "right", + "justify", + "both", + "distribute" + ], + "aliases": [ + "alignment", + "halign" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop align=center" + ], + "readback": "one of values", + "enforcement": "strict" + }, + "style": { + "type": "string", + "description": "paragraph styleId (e.g. Heading1, Normal, Quote). Must reference an existing style or one of the built-in style aliases. Aliases mirror the canonical readback keys exposed by Get: styleId targets the OOXML styleId; styleName resolves the display name through the styles part (lenient, falls back to verbatim if no match).", + "aliases": [ + "styleId", + "styleid", + "styleName", + "stylename" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop style=Heading1", + "--prop styleId=Heading1", + "--prop styleName=\"Heading 1\"" + ], + "readback": "styleId as stored on the paragraph", + "enforcement": "strict" + }, + "spaceBefore": { + "type": "length", + "aliases": [ + "spacebefore" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop spaceBefore=12pt" + ], + "readback": "unit-qualified, e.g. \"12pt\"", + "enforcement": "strict" + }, + "spaceAfter": { + "type": "length", + "aliases": [ + "spaceafter" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop spaceAfter=6pt" + ], + "readback": "unit-qualified, e.g. \"6pt\"", + "enforcement": "strict" + }, + "listStyle": { + "type": "enum", + "values": [ + "bullet", + "ordered", + "none" + ], + "description": "high-level list type. 'bullet' (aliases: unordered, ul) creates a bulleted list; 'ordered' (or any other non-bullet value, e.g. 'decimal') creates a numbered list; 'none'/'remove'/'clear' strips list formatting. Preferred over raw numId. Continues a preceding list of the same type automatically unless 'start' is also given.", + "aliases": [ + "liststyle" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop listStyle=bullet --prop text=\"item\"", + "--prop listStyle=ordered --prop text=\"step 1\"" + ], + "readback": "'bullet' or 'ordered' (normalized from the numbering format)", + "enforcement": "strict" + }, + "numId": { + "type": "int", + "description": "numbering definition id (w:numId). Low-level entry point — prefer 'listStyle' unless you specifically need to reference an existing numbering instance. Requires the numId to already exist in /numbering (create via `add /numbering --type num` first).", + "aliases": [ + "numid" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop numId=1" + ], + "readback": "numbering id as stored on the paragraph", + "enforcement": "report" + }, + "numLevel": { + "type": "int", + "description": "list indent level (w:ilvl), 0..8. Requires numId or listStyle to be effective; Get only surfaces numLevel when numId is present on the paragraph.", + "aliases": [ + "numlevel", + "ilvl", + "listLevel", + "listlevel", + "level" + ], + "requires": [ + "numId" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop numLevel=1 --prop numId=1", + "--prop ilvl=1 --prop numId=1" + ], + "readback": "integer level as stored on the paragraph (only when numId is set)", + "enforcement": "report" + }, + "start": { + "type": "int", + "description": "starting number for an ordered list (w:start on level 0 of the numbering definition). Only meaningful together with liststyle=ordered or an existing numid. Readback not implemented — w:start lives in the separate numbering part and cross-part traversal is fragile; query the numbering directly if you need it.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop liststyle=ordered --prop start=5 --prop text=\"item\"" + ], + "readback": "n/a (write-only via paragraph; query numbering part)", + "enforcement": "report" + }, + "bold": { + "type": "bool", + "description": "run-level bold. On Add, applied to the implicit run created by 'text'. On Set, applied to all runs in the paragraph and to the paragraph-mark run properties so subsequent runs inherit.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop bold=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "italic": { + "type": "bool", + "description": "run-level italic. Same scope as 'bold'.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop italic=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "font": { + "type": "string", + "description": "run-level font family (applied to Ascii/HighAnsi/EastAsia). On Add, applied to the implicit run created by 'text'. On Set, applied to all runs in the paragraph.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop font=\"Times New Roman\" --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "size": { + "type": "font-size", + "description": "run-level font size. Accepts bare number (pt), '14pt', '10.5pt'.", + "aliases": [ + "fontsize" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop size=14 --prop text=\"Hi\"", + "--prop size=10.5pt --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "color": { + "type": "color", + "description": "run-level text color. Accepts #RRGGBB, RRGGBB, named colors (e.g. red), rgb(r,g,b).", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop color=red --prop text=\"Hi\"", + "--prop color=#FF0000 --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "underline": { + "type": "string", + "description": "run-level underline. Accepts 'true'/'false' or an underline style (single, double, thick, dotted, dash, wavy, etc.).", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop underline=true --prop text=\"Hi\"", + "--prop underline=double --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "underline.color": { + "type": "color", + "description": "underline stroke color, written to w:u/@w:color. Preserved when 'underline' style is toggled.", + "aliases": [ + "underlineColor", + "underlinecolor", + "font.underline.color" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop underline=single --prop underline.color=#FF0000" + ], + "enforcement": "strict" + }, + "strike": { + "type": "bool", + "description": "run-level single strikethrough.", + "aliases": [ + "strikethrough" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop strike=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "highlight": { + "type": "string", + "description": "run-level highlight color (w:highlight values: yellow, green, cyan, magenta, blue, red, darkBlue, darkCyan, darkGreen, darkMagenta, darkRed, darkYellow, darkGray, lightGray, black, white, none).", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop highlight=yellow --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "caps": { + "type": "bool", + "description": "run-level all caps. On Add only (no paragraph-level Set wrapper).", + "aliases": [ + "allCaps" + ], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop caps=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "smallcaps": { + "type": "bool", + "description": "run-level small caps. On Add only.", + "aliases": [ + "smallCaps" + ], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop smallcaps=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "dstrike": { + "type": "bool", + "description": "run-level double strikethrough. On Add only.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop dstrike=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "vanish": { + "type": "bool", + "description": "run-level hidden text. On Add only.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop vanish=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "outline": { + "type": "bool", + "description": "run-level outline text effect. On Add only.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop outline=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "shadow": { + "type": "bool", + "description": "run-level shadow text effect. On Add only.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop shadow=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "emboss": { + "type": "bool", + "description": "run-level emboss text effect. On Add only.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop emboss=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "imprint": { + "type": "bool", + "description": "run-level imprint (engrave) text effect. On Add only.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop imprint=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "noproof": { + "type": "bool", + "description": "run-level no-proofing flag. On Add only.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop noproof=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "superscript": { + "type": "bool", + "description": "run-level superscript vertical alignment. On Add only (use vertAlign for Set).", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop superscript=true --prop text=\"x^2\"" + ], + "enforcement": "strict" + }, + "subscript": { + "type": "bool", + "description": "run-level subscript vertical alignment. On Add only (use vertAlign for Set).", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop subscript=true --prop text=\"H2O\"" + ], + "enforcement": "strict" + }, + "vertAlign": { + "type": "enum", + "values": [ + "superscript", + "subscript", + "baseline", + "super", + "sub" + ], + "description": "run-level vertical text alignment. On Add only.", + "aliases": [ + "vertalign" + ], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop vertAlign=superscript --prop text=\"x^2\"" + ], + "enforcement": "strict" + }, + "charspacing": { + "type": "length", + "description": "run-level character spacing in points (bare number = pt, or 'Xpt'). Stored as twips. On Add only.", + "aliases": [ + "charSpacing", + "letterspacing", + "letterSpacing" + ], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop charspacing=2 --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "rtl": { + "type": "bool", + "description": "run-level right-to-left text flag. On Add only.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop rtl=true --prop text=\"Hi\"" + ], + "enforcement": "strict" + }, + "direction": { + "type": "enum", + "values": [ + "ltr", + "rtl" + ], + "description": "paragraph reading direction. 'rtl' writes on pPr, on the paragraph mark, and on every run (so Arabic / Hebrew character order reverses inside runs, not just page-side layout). 'ltr' clears all three.", + "aliases": [ + "dir", + "bidi" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop direction=rtl" + ], + "readback": "rtl | ltr (only emitted when explicitly set)", + "enforcement": "strict" + }, + "font.cs": { + "type": "string", + "description": "Complex-script font slot (rFonts/cs) — Arabic / Hebrew / Thai typefaces.", + "aliases": [ + "font.complexscript", + "font.complex" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.cs=\"Arabic Typesetting\"" + ], + "enforcement": "report" + }, + "font.ea": { + "type": "string", + "description": "East-Asian font slot (rFonts/eastAsia) — Chinese / Japanese / Korean typefaces.", + "aliases": [ + "font.eastasia", + "font.eastasian" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.ea=\"メイリオ\"" + ], + "enforcement": "report" + }, + "font.latin": { + "type": "string", + "description": "Latin font slots (rFonts/ascii + hAnsi) — ASCII / Western text.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.latin=Calibri" + ], + "enforcement": "report" + }, + "font.asciiTheme": { + "type": "string", + "description": "Theme font binding for the ascii slot (rFonts/asciiTheme). Values: minorHAnsi, majorHAnsi, minorEastAsia, majorEastAsia, minorBidi, majorBidi.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.asciiTheme=minorHAnsi" + ], + "enforcement": "report" + }, + "font.hAnsiTheme": { + "type": "string", + "description": "Theme font binding for the hAnsi slot (rFonts/hAnsiTheme).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.hAnsiTheme=minorHAnsi" + ], + "enforcement": "report" + }, + "font.eaTheme": { + "type": "string", + "description": "Theme font binding for the East-Asia slot (rFonts/eastAsiaTheme). Values: minorEastAsia, majorEastAsia.", + "aliases": [ + "font.eastAsiaTheme" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.eaTheme=minorEastAsia" + ], + "enforcement": "report" + }, + "font.csTheme": { + "type": "string", + "description": "Theme font binding for the complex-script slot (rFonts/cstheme). Values: minorBidi, majorBidi.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.csTheme=minorBidi" + ], + "enforcement": "report" + }, + "bold.cs": { + "type": "bool", + "description": "complex-script bold for the paragraph's runs (). Required for Arabic / Hebrew bold rendering.", + "aliases": [ + "font.bold.cs", + "boldcs" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop bold.cs=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "italic.cs": { + "type": "bool", + "description": "complex-script italic () for the paragraph's runs.", + "aliases": [ + "font.italic.cs", + "italiccs" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop italic.cs=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "size.cs": { + "type": "font-size", + "description": "complex-script font size () for the paragraph's runs.", + "aliases": [ + "font.size.cs", + "sizecs" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop size.cs=14pt" + ], + "readback": "unit-qualified, e.g. \"14pt\"", + "enforcement": "strict" + }, + "shd": { + "type": "string", + "description": "shading. Format: 'fill' or 'val;fill' or 'val;fill;color'. Applied at paragraph level on Add (pPr/shd).", + "aliases": [ + "shading" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop shd=FFFF00", + "--prop shd=clear;FFFF00" + ], + "enforcement": "report" + }, + "firstLineIndent": { + "type": "length", + "description": "first-line indent. Routed through SpacingConverter.", + "aliases": [ + "firstlineindent" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop firstLineIndent=2cm" + ], + "enforcement": "report" + }, + "rightIndent": { + "type": "length", + "description": "right indentation. Routed through SpacingConverter.", + "aliases": [ + "rightindent", + "indentright" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop rightIndent=1cm" + ], + "enforcement": "report" + }, + "hangingIndent": { + "type": "length", + "description": "hanging indent (pairs with left indent). Routed through SpacingConverter.", + "aliases": [ + "hangingindent", + "hanging" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop hangingIndent=0.5cm" + ], + "readback": "unit-qualified length (e.g. \"28.35pt\")", + "enforcement": "report" + }, + "keepNext": { + "type": "bool", + "description": "keep paragraph with the next paragraph (no page break between).", + "aliases": [ + "keepnext" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop keepNext=true" + ], + "enforcement": "report" + }, + "keepLines": { + "type": "bool", + "description": "keep all lines of the paragraph together (no page break within).", + "aliases": [ + "keeplines", + "keeptogether", + "keepTogether" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop keepLines=true" + ], + "enforcement": "report" + }, + "pageBreakBefore": { + "type": "bool", + "description": "force a page break before this paragraph.", + "aliases": [ + "pagebreakbefore", + "break" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop pageBreakBefore=true", + "--prop break=newPage" + ], + "enforcement": "report" + }, + "widowControl": { + "type": "bool", + "description": "widow/orphan control.", + "aliases": [ + "widowcontrol" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop widowControl=true" + ], + "enforcement": "report" + }, + "wordWrap": { + "type": "bool", + "description": "Latin-word break behaviour in CJK paragraphs. Set false to allow ASCII text/whitespace to participate in CJK character flow — required for right-aligned CJK lines that rely on trailing underlined whitespace to align with adjacent lines.", + "aliases": [ + "wordwrap" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop wordWrap=false" + ], + "enforcement": "report" + }, + "contextualSpacing": { + "type": "bool", + "description": "suppress space between paragraphs of the same style. Applied on Add/Set to paragraph pPr; also valid on Style.", + "aliases": [ + "contextualspacing" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop contextualSpacing=true" + ], + "enforcement": "report" + }, + "effective.size": { + "type": "font-size", + "description": "inheritance-resolved font size (read-only) — derived from the first run's style chain → paragraph style → docDefaults.", + "add": false, + "set": false, + "get": true, + "readback": "unit-qualified, e.g. \"14pt\"", + "enforcement": "report" + }, + "effective.size.src": { + "type": "string", + "description": "source pointer for effective.size.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "effective.font.ascii": { + "type": "string", + "description": "inheritance-resolved Latin/ASCII font slot (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "effective.font.ascii.src": { + "type": "string", + "description": "source pointer for effective.font.ascii.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "effective.font.eastAsia": { + "type": "string", + "description": "inheritance-resolved East-Asian font slot (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "effective.font.eastAsia.src": { + "type": "string", + "description": "source pointer for effective.font.eastAsia.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "effective.font.hAnsi": { + "type": "string", + "description": "inheritance-resolved High-ANSI font slot (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "effective.font.hAnsi.src": { + "type": "string", + "description": "source pointer for effective.font.hAnsi.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "effective.font.cs": { + "type": "string", + "description": "inheritance-resolved complex-script font slot (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "effective.font.cs.src": { + "type": "string", + "description": "source pointer for effective.font.cs.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "effective.bold": { + "type": "bool", + "description": "inheritance-resolved bold (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "effective.bold.src": { + "type": "string", + "description": "source pointer for effective.bold.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "effective.italic": { + "type": "bool", + "description": "inheritance-resolved italic (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "effective.italic.src": { + "type": "string", + "description": "source pointer for effective.italic.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "effective.color": { + "type": "color", + "description": "inheritance-resolved font color (read-only). #RRGGBB or scheme color name.", + "add": false, + "set": false, + "get": true, + "readback": "#RRGGBB uppercase or scheme color", + "enforcement": "report" + }, + "effective.color.src": { + "type": "string", + "description": "source pointer for effective.color.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "effective.underline": { + "type": "string", + "description": "inheritance-resolved underline style (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "underline style name", + "enforcement": "report" + }, + "effective.underline.src": { + "type": "string", + "description": "source pointer for effective.underline.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "effective.rtl": { + "type": "bool", + "description": "inheritance-resolved right-to-left flag (read-only). Emitted even when 'rtl' is set directly so callers can compare direct vs cascade-resolved state.", + "add": false, + "set": false, + "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "effective.rtl.src": { + "type": "string", + "description": "source pointer for effective.rtl.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "numFmt": { + "type": "string", + "description": "raw numbering format (e.g. bullet, decimal, lowerLetter). Emitted only when present.", + "add": false, + "set": false, + "get": true, + "readback": "numbering format token", + "enforcement": "report" + }, + "shading.val": { + "type": "string", + "description": "shading pattern value (decomposed from `shd`). Add/Set use `shd`.", + "add": false, + "set": false, + "get": true, + "readback": "shading pattern token", + "enforcement": "report" + }, + "shading.fill": { + "type": "string", + "description": "shading fill color hex (decomposed from `shd`).", + "add": false, + "set": false, + "get": true, + "readback": "#RRGGBB", + "enforcement": "report" + }, + "shading.color": { + "type": "string", + "description": "shading foreground color hex.", + "add": false, + "set": false, + "get": true, + "readback": "#RRGGBB", + "enforcement": "report" + }, + "paraId": { + "type": "string", + "description": "paragraph stable id (source of @paraId in stable path). Emitted only when present.", + "add": false, + "set": false, + "get": true, + "readback": "paraId hex string", + "enforcement": "report" + }, + "outlineLvl": { + "type": "number", + "description": "outline level (0-9). Used by Word's TOC and document map.", + "add": true, + "set": true, + "get": true, + "readback": "integer", + "enforcement": "report" + }, + "rStyle": { + "type": "string", + "description": "Paragraph mark run style id (e.g. FootnoteReference, IntenseEmphasis). Inherited by runs without their own rStyle.", + "add": true, + "set": true, + "get": true, + "readback": "style id", + "enforcement": "report" + }, + "tabs": { + "type": "array", + "description": "tab stops array. Emitted only when present.", + "add": false, + "set": false, + "get": true, + "readback": "array of tab stop descriptors", + "enforcement": "report" + }, + "pbdr.top": { + "type": "string", + "description": "paragraph border edge descriptor. Emitted only when present.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (handler does not surface paragraph borders today)", + "enforcement": "report" + }, + "pbdr.bottom": { + "type": "string", + "description": "paragraph border edge descriptor. Emitted only when present.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (handler does not surface paragraph borders today)", + "enforcement": "report" + }, + "pbdr.left": { + "type": "string", + "description": "paragraph border edge descriptor. Emitted only when present.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (handler does not surface paragraph borders today)", + "enforcement": "report" + }, + "pbdr.right": { + "type": "string", + "description": "paragraph border edge descriptor. Emitted only when present.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (handler does not surface paragraph borders today)", + "enforcement": "report" + }, + "pbdr.between": { + "type": "string", + "description": "paragraph border edge descriptor. Emitted only when present.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (handler does not surface paragraph borders today)", + "enforcement": "report" + }, + "pbdr.bar": { + "type": "string", + "description": "paragraph border edge descriptor. Emitted only when present.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (handler does not surface paragraph borders today)", + "enforcement": "report" + }, + "firstLineChars": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "first-line indent in 1/100 character units (CT_Ind @firstLineChars). Word's chars-relative variant of firstLineIndent.", + "readback": "integer 1/100-char units", + "enforcement": "report" + }, + "hangingChars": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "hanging indent in 1/100 character units (CT_Ind @hangingChars). Word's chars-relative variant of hangingIndent.", + "readback": "integer 1/100-char units", + "enforcement": "report" + }, + "markRPr.bold": { + "type": "bool", + "description": "paragraph-mark run property — bold flag on the ¶ glyph (w:pPr/w:rPr/w:b). Distinct from per-run bold; affects how the mark itself renders and is inherited by appended runs without their own bold setting.", + "add": false, + "set": false, + "get": true, + "readback": "true|false", + "enforcement": "report" + }, + "markRPr.italic": { + "type": "bool", + "description": "paragraph-mark run italic (w:pPr/w:rPr/w:i).", + "add": false, + "set": false, + "get": true, + "readback": "true|false", + "enforcement": "report" + }, + "markRPr.strike": { + "type": "bool", + "description": "paragraph-mark run strike (w:pPr/w:rPr/w:strike).", + "add": false, + "set": false, + "get": true, + "readback": "true|false", + "enforcement": "report" + }, + "markRPr.underline": { + "type": "string", + "description": "paragraph-mark run underline style (w:pPr/w:rPr/w:u @val).", + "add": false, + "set": false, + "get": true, + "readback": "underline style enum (single, double, dotted, …)", + "enforcement": "report" + }, + "markRPr.size": { + "type": "length", + "description": "paragraph-mark run font size (w:pPr/w:rPr/w:sz, half-points internally).", + "add": false, + "set": false, + "get": true, + "readback": "unit-qualified pt (e.g. 11pt)", + "enforcement": "report" + }, + "markRPr.color": { + "type": "color", + "description": "paragraph-mark run color (w:pPr/w:rPr/w:color). Returns scheme color name for theme refs, or #-prefixed hex.", + "add": false, + "set": false, + "get": true, + "readback": "scheme color name or #RRGGBB", + "enforcement": "report" + }, + "markRPr.highlight": { + "type": "string", + "description": "paragraph-mark run highlight color (w:pPr/w:rPr/w:highlight @val).", + "add": false, + "set": false, + "get": true, + "readback": "highlight color enum", + "enforcement": "report" + }, + "markRPr.font.latin": { + "type": "string", + "description": "paragraph-mark run Ascii font (w:pPr/w:rPr/w:rFonts @ascii).", + "add": false, + "set": false, + "get": true, + "readback": "font name", + "enforcement": "report" + }, + "markRPr.font.ea": { + "type": "string", + "description": "paragraph-mark run EastAsia font (w:pPr/w:rPr/w:rFonts @eastAsia).", + "add": false, + "set": false, + "get": true, + "readback": "font name", + "enforcement": "report" + }, + "markRPr.font.cs": { + "type": "string", + "description": "paragraph-mark run ComplexScript font (w:pPr/w:rPr/w:rFonts @cs).", + "add": false, + "set": false, + "get": true, + "readback": "font name", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/picture.json b/schemas/help/docx/picture.json new file mode 100644 index 000000000..b56ebcdbb --- /dev/null +++ b/schemas/help/docx/picture.json @@ -0,0 +1,127 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "picture", + "parent": "paragraph", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/body/p[@paraId=X]/r[N]" + ] + }, + "note": "Aliases: image, img. 'src' is required (alias 'path'). Image source resolved via ImageSource — accepts file path, URL, data-URI, or raw bytes. SVGs auto-generate a PNG fallback.", + "extends": [ + "_shared/picture", + "_shared/picture.docx-pptx", + "_shared/picture.docx-xlsx" + ], + "properties": { + "anchor": { + "type": "bool", + "add": true, + "set": false, + "get": true, + "description": "true to create a floating (anchored) picture instead of inline. Required to enable wrap/hPosition/vPosition/hRelative/vRelative/behindText.", + "examples": [ + "--prop anchor=true --prop wrap=square" + ], + "readback": "true on floating pictures", + "enforcement": "report" + }, + "wrap": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "text wrapping mode for floating picture (none, square, tight, topandbottom, through). For 'behind text', use wrap=none + behindText=true.", + "examples": [ + "--prop wrap=square" + ], + "readback": "wrap token", + "enforcement": "report" + }, + "behindText": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "description": "true when the picture floats behind text (anchor @behindDoc=1).", + "readback": "true on behind-text floats", + "enforcement": "report" + }, + "hPosition": { + "type": "length", + "add": true, + "set": true, + "get": true, + "description": "absolute horizontal anchor position in cm (positionH/posOffset).", + "examples": [ + "--prop hPosition=3cm" + ], + "readback": "length in cm (e.g. `5.0cm`)", + "enforcement": "report" + }, + "vPosition": { + "type": "length", + "add": true, + "set": true, + "get": true, + "description": "absolute vertical anchor position in cm (positionV/posOffset).", + "examples": [ + "--prop vPosition=4cm" + ], + "readback": "length in cm (e.g. `4.0cm`)", + "enforcement": "report" + }, + "hRelative": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "horizontal anchor reference frame (e.g. page, margin, column, character).", + "readback": "OOXML positionH @relativeFrom token", + "enforcement": "report" + }, + "vRelative": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "vertical anchor reference frame (e.g. page, margin, paragraph, line).", + "readback": "OOXML positionV @relativeFrom token", + "enforcement": "report" + }, + "width": { + "type": "length", + "description": "width — cm length (extent.Cy/Cx in EMU formatted to cm).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop width=5", + "--prop width=3in" + ], + "readback": "length string", + "enforcement": "report" + }, + "height": { + "type": "length", + "description": "height — cm length (extent.Cy/Cx in EMU formatted to cm).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop height=5", + "--prop height=2in" + ], + "readback": "length string", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/ptab.json b/schemas/help/docx/ptab.json new file mode 100644 index 000000000..66bbe0f39 --- /dev/null +++ b/schemas/help/docx/ptab.json @@ -0,0 +1,82 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "ptab", + "parent": "paragraph", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/body/p[@paraId=X]/r[N]", + "/header[N]/p[M]/r[K]", + "/footer[N]/p[M]/r[K]" + ] + }, + "note": "Inline positional tab (w:ptab, Word 2007+). Anchors left/center/right alignment regions in headers/footers. Inserted as ; surfaced by Get as type=ptab. Aliases: positionaltab.", + "properties": { + "align": { + "type": "enum", + "values": [ + "left", + "center", + "right" + ], + "aliases": [ + "alignment" + ], + "add": true, + "set": true, + "get": true, + "required": true, + "examples": [ + "--prop align=center", + "--prop align=right" + ], + "readback": "ptab alignment attribute", + "enforcement": "report" + }, + "relativeTo": { + "type": "enum", + "values": [ + "margin", + "indent" + ], + "default": "margin", + "aliases": [ + "relativeto" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop relativeTo=margin" + ], + "readback": "ptab relativeTo attribute", + "enforcement": "report" + }, + "leader": { + "type": "enum", + "values": [ + "none", + "dot", + "hyphen", + "middleDot", + "underscore" + ], + "default": "none", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop leader=dot" + ], + "readback": "ptab leader attribute", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/raw.json b/schemas/help/docx/raw.json new file mode 100644 index 000000000..e26bf4694 --- /dev/null +++ b/schemas/help/docx/raw.json @@ -0,0 +1,32 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "raw", + "operations": { + "add": false, + "set": false, + "get": false, + "query": false, + "remove": false + }, + "properties": {}, + "description": "Raw OOXML access via the `raw` (read) and `raw-set` (write) commands. Use only when L2 DOM operations cannot express what you need. Two part-path forms are accepted: (1) semantic short names (recommended) and (2) zip-internal URIs (any path ending in .xml is resolved literally against the package, e.g. /word/document.xml, /word/footnotes.xml).", + "parts": [ + { "name": "/document", "desc": "Main document body" }, + { "name": "/styles", "desc": "Style definitions" }, + { "name": "/settings", "desc": "Document-level settings (compatibility, view, protection)" }, + { "name": "/numbering", "desc": "Numbering / list definitions" }, + { "name": "/comments", "desc": "Comments part" }, + { "name": "/theme", "desc": "Theme (color scheme, font scheme)" }, + { "name": "/header[N]", "desc": "Nth header part (1-based)" }, + { "name": "/footer[N]", "desc": "Nth footer part (1-based)" }, + { "name": "/chart[N]", "desc": "Nth embedded chart" }, + { "name": "/.xml", "desc": "Any path ending in .xml is resolved as a literal zip-internal URI (e.g. /word/footnotes.xml, /word/endnotes.xml, /word/glossary/document.xml, /customXml/item1.xml). Use for parts not covered by the semantic shortnames." } + ], + "examples": [ + "officecli raw report.docx /document", + "officecli raw report.docx /styles", + "officecli raw report.docx /word/footnotes.xml", + "officecli raw-set report.docx /document --xpath \"//w:p[1]\" --action remove" + ] +} diff --git a/schemas/help/docx/revision.json b/schemas/help/docx/revision.json new file mode 100644 index 000000000..5e9278bbd --- /dev/null +++ b/schemas/help/docx/revision.json @@ -0,0 +1,172 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "revision", + "parent": "paragraph|run|table|row|cell|section", + "operations": { + "add": false, + "set": true, + "get": true, + "query": true, + "remove": false, + "note": "`set: true` covers the action verb (revision.action on a /revision selector or native path). `add: false` here means the *revision element itself* has no Add surface — tracked changes are created indirectly via `revision.type` / `revision.author` / `revision.date` / `revision.id` on the host run/paragraph/table/row/cell/section, where they read as `add: true` / `set: true`. Per-property add/set flags below describe behavior on the synthetic /revision node, not on the host." + }, + "paths": { + "positional": ["/revision[N]", "/body/p[N]/ins[M]", "/body/p[N]/del[M]"], + "selector": ["/revision", "/revision[@id=N]", "/revision[@id=N][@type=moveFrom|moveTo]", "/revision[@author=NAME]", "/revision[@type=ins|del|format|moveFrom|moveTo]", "/revision[@author=NAME][@type=TYPE]"] + }, + "note": "Tracked-change marker. `query revision` enumerates every w:ins / w:del / w:moveFrom / w:moveTo / w:*PrChange in the document. The synthetic Path is `/revision[@id=N]` when the marker carries a stable w:id (true for everything Word/WPS writes), falling back to positional `/revision[N]` only for markers without an id — same precedence as paragraphs (`p[@paraId=…]` → `p[N]`). When the same w:id is shared by more than one marker (canonical case: a moveFrom/moveTo pair binds via shared id), the Path is disambiguated as `/revision[@id=N][@type=moveFrom]` / `[@type=moveTo]` so single-end addressing stays unique. `set /revision[@id=N]` without the type segment still acts on both ends together (pair-wise accept/reject, the usual move-revision intent). Use the @id= form in scripts; positional indexes shift after accept/reject. Both `get /revision[@id=N]` and `set /revision[@id=N] --prop revision.action=…` accept the same path. Two disjoint property namespaces: `revision.type=ins|del|format|moveFrom|moveTo` creates a tracked change (used with add/set on the host run/paragraph/table/row/cell/section), while `revision.action=accept|reject` acts on existing markers via /revision selectors or native paths (/body/p[N], /body/p[N]/r[M], etc.). The two namespaces are mutually exclusive — mixing revision.action with any revision.type/.author/.date/.id key is rejected as ambiguous. Bare `revision=…` is no longer accepted. **Find + revision** (matches Word's Find&Replace with Track Changes on): `set --prop find=PATTERN [--prop regex=true] --prop revision.author=NAME` infers the marker shape from the operation — `--prop replace=NEW` produces a w:del(old) + w:ins(new) pair, `--prop replace=` (empty) produces w:del only, format props (bold, font.color, …) produce one w:rPrChange per matched run, and paragraph-scope props (align, indent, …) produce one w:pPrChange per matched paragraph. The handler auto-allocates a fresh revision.id per marker, so `--prop revision.id=…` is rejected on find (would collide across markers). `revision.type=ins|del|moveFrom|moveTo` and `revision.action=…` are also rejected on find — the shape is inferred and the action verb belongs on the selector, not the creation context. `find` / `replace` / `regex` themselves are cross-cutting Set keywords (not revision-specific properties; not listed below) — pairing any of them with `revision.author` routes the find pass through the track-changes pipeline. Separate from `revisionNumber` on / (document save counter) and `trackChanges` on /settings (track-mode toggle).", + "properties": { + "type": { + "type": "enum", + "values": ["ins", "del", "moveTo", "moveFrom", "format"], + "add": false, "set": false, "get": true, + "examples": ["query /revision[@type=ins]"], + "readback": "revision type (read-only on the revision node; create with --prop revision.type=… on the host element)", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "text content for ins / content marker for del (read-only).", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "concatenated text (read-only)", + "enforcement": "report" + }, + "author": { + "type": "string", + "description": "Author of the revision. Surfaced on `query revision` nodes and on host elements (run/paragraph/…) that carry an ins/del/*PrChange ancestor. Supply via --prop revision.author=… on the host element to create. **Run host is stricter:** `set --prop revision.author=X` alone is rejected — without an action to attribute, the legacy fallback wrote an empty-snapshot rPrChange (recording no change). Either pair revision.author with a real format prop (`--prop bold=true`), or pass `--prop revision.type=ins|del|moveFrom|moveTo` so the wrap itself is the action. The same strictness applies to bare `revision.type=format` on Run. Non-Run hosts (paragraph/table/row/cell/section) still accept bare attribution and emit an empty-snapshot *PrChange — needed for dump→batch round-trip of table-level markers whose pre-change baseline can't be reconstructed. Also accepted alongside `find` (and `replace` / format / paragraph props) — the find dispatch wraps each match in the matching marker shape and attributes it to this author.", + "add": false, "set": false, "get": true, + "examples": [ + "query /revision[@author=Alice]", + "set /body --prop find=fox --prop replace=cat --prop revision.author=Alice", + "set /body --prop find=red --prop bold=true --prop revision.author=Bob", + "set /body --prop find=BAD --prop replace= --prop revision.author=Carol", + "set /body --prop 'find=\\$\\d+' --prop regex=true --prop bold=true --prop revision.author=Dave" + ], + "readback": "revision author (read-only on the revision node)", + "enforcement": "report" + }, + "date": { + "type": "string", + "description": "ISO-8601 timestamp (read-only on the revision node). Supply via --prop revision.date=… on the host element to create.", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "Date attribute (read-only)", + "enforcement": "report" + }, + "id": { + "type": "integer", + "description": "w:id attribute. Stable across save; pair moveFrom/moveTo by reusing the same id. Supply via --prop revision.id=… on the host element.", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "revision id (read-only on the revision node)", + "enforcement": "report" + }, + "nativePath": { + "type": "string", + "description": "Path to the underlying w:ins/w:del/w:moveFrom/w:moveTo/w:*PrChange element in document DOM terms (e.g. /body/p[2]/ins[1]). Use it to address the revision via the action native-path form: `set --prop revision.action=accept|reject`. Emitted on `query revision` synthetic nodes only.", + "add": false, "set": false, "get": true, + "examples": ["set /body/p[2]/ins[1] --prop revision.action=accept"], + "readback": "DOM path to the revision element (read-only)", + "enforcement": "report" + }, + "action": { + "type": "enum", + "values": ["accept", "reject"], + "description": "Accept / reject verb. Lives in a disjoint namespace from the creation keys above; mixing with revision.type/.author/.date/.id throws. Walks matching elements in reverse document order so /revision[N] paths don't shift mid-batch.", + "add": false, "set": true, "get": false, + "examples": [ + "set /revision --prop revision.action=accept", + "set '/revision[@author=Alice]' --prop revision.action=reject", + "set '/revision[@type=ins]' --prop revision.action=accept", + "set /revision[@id=42] --prop revision.action=accept", + "set '/revision[@id=42][@type=moveTo]' --prop revision.action=reject", + "set /revision[3] --prop revision.action=accept", + "set /body/p[1]/r[2] --prop revision.action=reject" + ], + "readback": "(action is a verb — not surfaced on get)", + "enforcement": "strict" + }, + "paraMarkIns.author": { + "type": "string", + "description": "Author of a paragraph-mark-only insertion (w:rPr/w:ins inside the paragraph's pPr/rPr — the ¶ mark itself is part of the inserted change, distinct from a run-level w:ins wrapping content). Surfaced on paragraph nodes via get/query; on dump→batch the emitter rewrites it to bare revision.author on the synthetic paragraph + strips revision.type so the bare-attribution path defaults the type to ins.", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "paragraph-mark insertion author (read-only)", + "enforcement": "report" + }, + "paraMarkIns.date": { + "type": "string", + "description": "ISO-8601 timestamp companion to paraMarkIns.author.", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "paragraph-mark insertion date (read-only)", + "enforcement": "report" + }, + "sectPrChange.author": { + "type": "string", + "description": "Author of a w:sectPrChange on a section (revision.type=format equivalent at section scope; surfaced as a distinct key because the snapshot lives in a different OOXML wrapper than rPrChange/pPrChange).", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "section property-change author (read-only)", + "enforcement": "report" + }, + "sectPrChange.date": { + "type": "string", + "description": "ISO-8601 timestamp companion to sectPrChange.author.", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "section property-change date (read-only)", + "enforcement": "report" + }, + "tblPrChange.author": { + "type": "string", + "description": "Author of a w:tblPrChange on a table (table-properties revision — borders/style/layout snapshot).", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "table property-change author (read-only)", + "enforcement": "report" + }, + "tblPrChange.date": { + "type": "string", + "description": "ISO-8601 timestamp companion to tblPrChange.author.", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "table property-change date (read-only)", + "enforcement": "report" + }, + "trPrChange.author": { + "type": "string", + "description": "Author of a w:trPrChange on a table row (row-properties revision — row height/header/cantSplit snapshot). Separate from w:trPr/w:ins (= row insertion as tracked change).", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "row property-change author (read-only)", + "enforcement": "report" + }, + "trPrChange.date": { + "type": "string", + "description": "ISO-8601 timestamp companion to trPrChange.author.", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "row property-change date (read-only)", + "enforcement": "report" + }, + "tcPrChange.author": { + "type": "string", + "description": "Author of a w:tcPrChange on a table cell (cell-properties revision — borders/shading/width snapshot). Separate from w:tcPr/w:cellIns/w:cellDel (= cell insertion/deletion).", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "cell property-change author (read-only)", + "enforcement": "report" + }, + "tcPrChange.date": { + "type": "string", + "description": "ISO-8601 timestamp companion to tcPrChange.author.", + "add": false, "set": false, "get": true, + "examples": [], + "readback": "cell property-change date (read-only)", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/run.json b/schemas/help/docx/run.json new file mode 100644 index 000000000..5a8fc90c1 --- /dev/null +++ b/schemas/help/docx/run.json @@ -0,0 +1,706 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "run", + "elementAliases": ["r"], + "parent": "paragraph", + "operations": { + "add": true, + "set": true, + "get": true, + "remove": true + }, + "paths": { + "stable": [ + "/body/p[@paraId=ID]/r[N]" + ], + "positional": [ + "/body/p[N]/r[N]" + ] + }, + "note": "effective.* keys (effective.size, effective.bold, effective.color, ...) are read-only inheritance-resolved values: the run does not set the property directly, but resolves it by walking the run/paragraph style chain up to docDefaults. Each carries a paired effective.X.src pointer (e.g. \"/styles/Heading1\" or \"/docDefaults\") so callers can locate the writing layer. effective.* never appears when the run has the corresponding direct value — direct always wins.", + "extends": [ + "_shared/run", + "_shared/run.docx-pptx", + "_shared/run.docx-xlsx" + ], + "properties": { + "highlight": { + "type": "color", + "description": "Word built-in highlight color. Accepts named colors (yellow, green, cyan, magenta, blue, red, ...).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop highlight=yellow" + ], + "readback": "highlight color name", + "enforcement": "report" + }, + "strike": { + "type": "bool", + "description": "single strikethrough.", + "aliases": [ + "strikethrough", + "font.strike", + "font.strikethrough" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop strike=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "dstrike": { + "type": "bool", + "description": "double strikethrough.", + "aliases": [ + "doublestrike", + "doubleStrike" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop dstrike=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "caps": { + "type": "bool", + "description": "render text in all caps (display only; underlying text unchanged).", + "aliases": [ + "allCaps" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop caps=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "smallcaps": { + "type": "bool", + "description": "render lowercase as small caps.", + "aliases": [ + "smallCaps" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop smallcaps=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "vanish": { + "type": "bool", + "description": "hidden text (not rendered, but present in the file).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop vanish=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "outline": { + "type": "bool", + "description": "outline (text effect).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop outline=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "shadow": { + "type": "bool", + "description": "shadow (text effect).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop shadow=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "emboss": { + "type": "bool", + "description": "emboss (text effect).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop emboss=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "imprint": { + "type": "bool", + "description": "imprint / engrave (text effect).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop imprint=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "noproof": { + "type": "bool", + "description": "exclude this run from spell/grammar checking.", + "aliases": [ + "noProof" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop noproof=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "rtl": { + "type": "bool", + "description": "right-to-left text (legacy alias of 'direction'). Get surfaces this as direction=rtl|ltr.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop rtl=true" + ], + "enforcement": "strict" + }, + "direction": { + "type": "enum", + "values": [ + "ltr", + "rtl" + ], + "description": "run reading direction. Use 'rtl' for Arabic / Hebrew, 'ltr' to clear. Canonical key for run direction; matches paragraph/section vocabulary.", + "aliases": [ + "dir" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop direction=rtl" + ], + "readback": "rtl | ltr", + "enforcement": "strict" + }, + "font.cs": { + "type": "string", + "description": "Complex-script font slot (rFonts/cs) — Arabic / Hebrew / Thai typefaces.", + "aliases": [ + "font.complexscript", + "font.complex" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.cs=\"Arabic Typesetting\"" + ], + "enforcement": "report" + }, + "font.ea": { + "type": "string", + "description": "East-Asian font slot (rFonts/eastAsia) — Chinese / Japanese / Korean typefaces.", + "aliases": [ + "font.eastasia", + "font.eastasian" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.ea=\"メイリオ\"" + ], + "enforcement": "report" + }, + "font.latin": { + "type": "string", + "description": "Latin font slots (rFonts/ascii + hAnsi) — ASCII / Western text.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.latin=Calibri" + ], + "enforcement": "report" + }, + "font.asciiTheme": { + "type": "string", + "description": "Theme font binding for the ascii slot (rFonts/asciiTheme). Values: minorHAnsi, majorHAnsi, minorEastAsia, majorEastAsia, minorBidi, majorBidi.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.asciiTheme=minorHAnsi" + ], + "enforcement": "report" + }, + "font.hAnsiTheme": { + "type": "string", + "description": "Theme font binding for the hAnsi slot (rFonts/hAnsiTheme).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.hAnsiTheme=minorHAnsi" + ], + "enforcement": "report" + }, + "font.eaTheme": { + "type": "string", + "description": "Theme font binding for the East-Asia slot (rFonts/eastAsiaTheme). Values: minorEastAsia, majorEastAsia.", + "aliases": [ + "font.eastAsiaTheme" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.eaTheme=minorEastAsia" + ], + "enforcement": "report" + }, + "font.csTheme": { + "type": "string", + "description": "Theme font binding for the complex-script slot (rFonts/cstheme). Values: minorBidi, majorBidi.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.csTheme=minorBidi" + ], + "enforcement": "report" + }, + "bold.cs": { + "type": "bool", + "description": "complex-script bold (). Word renders Arabic / Hebrew bold via this flag, NOT . Required for Arabic bold to actually render.", + "aliases": [ + "font.bold.cs", + "boldcs" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop bold.cs=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "italic.cs": { + "type": "bool", + "description": "complex-script italic (). Same role as bold.cs for italic styling on Arabic / Hebrew text.", + "aliases": [ + "font.italic.cs", + "italiccs" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop italic.cs=true" + ], + "readback": "true | false", + "enforcement": "strict" + }, + "size.cs": { + "type": "font-size", + "description": "complex-script font size (). Independent from the bare 'size' () which only sizes Latin text.", + "aliases": [ + "font.size.cs", + "sizecs" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop size.cs=14pt", + "--prop size.cs=14" + ], + "readback": "unit-qualified, e.g. \"14pt\"", + "enforcement": "strict" + }, + "lang.latin": { + "type": "string", + "description": "Latin-script language tag (). e.g. en-US, fr-FR.", + "aliases": [ + "lang", + "lang.val" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop lang.latin=en-US" + ], + "readback": "BCP-47 / xml:lang tag, e.g. \"en-US\"", + "enforcement": "lenient" + }, + "lang.ea": { + "type": "string", + "description": "EastAsian-script language tag (). e.g. zh-CN, ja-JP.", + "aliases": [ + "lang.eastAsia", + "lang.eastAsian" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop lang.ea=zh-CN" + ], + "readback": "BCP-47 / xml:lang tag", + "enforcement": "lenient" + }, + "lang.cs": { + "type": "string", + "description": "ComplexScript language tag (). e.g. ar-SA, he-IL.", + "aliases": [ + "lang.complexScript", + "lang.bidi" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop lang.cs=ar-SA" + ], + "readback": "BCP-47 / xml:lang tag", + "enforcement": "lenient" + }, + "vertAlign": { + "type": "enum", + "description": "vertical text alignment. Values: superscript|super, subscript|sub, baseline.", + "aliases": [ + "vertalign" + ], + "values": [ + "superscript", + "super", + "subscript", + "sub", + "baseline" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop vertAlign=superscript" + ], + "readback": "n/a (surfaces as superscript/subscript flag)", + "enforcement": "report" + }, + "charSpacing": { + "type": "length", + "description": "character spacing (letter spacing) in points. Stored as twips × 20.", + "aliases": [ + "charspacing", + "letterspacing", + "letterSpacing", + "spacing" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop charSpacing=1pt", + "--prop charspacing=2" + ], + "readback": "unit-qualified pt, e.g. \"1pt\"", + "enforcement": "report" + }, + "shading": { + "type": "color", + "description": "background shading color or ';;' triplet.", + "aliases": [ + "shd" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop shading=FFFF00", + "--prop shd=clear;FFFF00;auto" + ], + "readback": "fill #RRGGBB", + "enforcement": "report" + }, + "textOutline": { + "type": "string", + "description": "w14 text outline 'WIDTHpt-COLOR' (e.g. '1pt-FF0000'). Width first, color second; '-' or ';' separator.", + "aliases": [ + "textoutline" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop textOutline=1pt-FF0000" + ], + "readback": "\"{width}pt\" or \"{width}pt;#RRGGBB\"", + "enforcement": "report" + }, + "textFill": { + "type": "string", + "description": "w14 text fill (color or gradient spec).", + "aliases": [ + "textfill" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop textFill=FF0000" + ], + "readback": "#RRGGBB (solid) | C1;C2;angle (gradient) | radial:C1;C2", + "enforcement": "report" + }, + "w14shadow": { + "type": "string", + "description": "w14 text shadow effect.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop w14shadow=FF0000" + ], + "readback": "#RRGGBB;blur_pt;angle_deg;dist_pt;opacity", + "enforcement": "report" + }, + "w14glow": { + "type": "string", + "description": "w14 text glow effect.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop w14glow=FF0000" + ], + "readback": "#RRGGBB;radius_pt;opacity", + "enforcement": "report" + }, + "w14reflection": { + "type": "string", + "description": "w14 text reflection effect.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop w14reflection=true" + ], + "readback": "semicolon-delimited reflection parameters", + "enforcement": "report" + }, + "effective.size.src": { + "type": "string", + "description": "source pointer for effective.size — path of the writing layer (e.g. \"/styles/Heading1\", \"/docDefaults\"). Documented but not emitted today; only the resolved `effective.size` value surfaces on Get.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (planned — only effective.size emits today)", + "enforcement": "report" + }, + "effective.font.ascii": { + "type": "string", + "description": "inheritance-resolved Latin/ASCII font slot (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "effective.font.ascii.src": { + "type": "string", + "description": "source pointer for effective.font.ascii. Documented but not emitted today.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (planned)", + "enforcement": "report" + }, + "effective.font.eastAsia": { + "type": "string", + "description": "inheritance-resolved East-Asian font slot (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "effective.font.eastAsia.src": { + "type": "string", + "description": "source pointer for effective.font.eastAsia. Documented but not emitted today.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (planned)", + "enforcement": "report" + }, + "effective.font.hAnsi": { + "type": "string", + "description": "inheritance-resolved High-ANSI font slot (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "effective.font.hAnsi.src": { + "type": "string", + "description": "source pointer for effective.font.hAnsi. Documented but not emitted today.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (planned)", + "enforcement": "report" + }, + "effective.font.cs": { + "type": "string", + "description": "inheritance-resolved complex-script font slot (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "font family name", + "enforcement": "report" + }, + "effective.font.cs.src": { + "type": "string", + "description": "source pointer for effective.font.cs. Documented but not emitted today.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (planned)", + "enforcement": "report" + }, + "effective.bold.src": { + "type": "string", + "description": "source pointer for effective.bold. Documented but not emitted today.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (planned)", + "enforcement": "report" + }, + "effective.italic": { + "type": "bool", + "description": "inheritance-resolved italic (read-only). Surfaced only when the run does not set 'italic' directly.", + "add": false, + "set": false, + "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "effective.italic.src": { + "type": "string", + "description": "source pointer for effective.italic. Documented but not emitted today.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (planned)", + "enforcement": "report" + }, + "effective.color.src": { + "type": "string", + "description": "source pointer for effective.color. Documented but not emitted today.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (planned)", + "enforcement": "report" + }, + "effective.underline": { + "type": "string", + "description": "inheritance-resolved underline style (read-only).", + "add": false, + "set": false, + "get": true, + "readback": "underline style name", + "enforcement": "report" + }, + "effective.underline.src": { + "type": "string", + "description": "source pointer for effective.underline. Documented but not emitted today.", + "add": false, + "set": false, + "get": false, + "readback": "n/a (planned)", + "enforcement": "report" + }, + "effective.rtl": { + "type": "bool", + "description": "inheritance-resolved right-to-left flag (read-only). Emitted even when 'rtl' is set directly so callers can compare direct vs cascade-resolved state.", + "add": false, + "set": false, + "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "effective.rtl.src": { + "type": "string", + "description": "source pointer for effective.rtl.", + "add": false, + "set": false, + "get": true, + "readback": "style/docDefaults path", + "enforcement": "report" + }, + "dirty": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "true when the run is flagged as dirty (rPr/dirty=1) — Word treats it as needing reflow on next open.", + "readback": "true|false", + "enforcement": "report" + }, + "font.ascii": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "individual rFonts @ascii slot readback. On Add/Set use the unified `font.latin` key (which writes both ascii + hAnsi).", + "readback": "font family name", + "enforcement": "report" + }, + "font.eastAsia": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "individual rFonts @eastAsia slot readback. On Add/Set use the `font.ea` key.", + "readback": "font family name", + "enforcement": "report" + }, + "font.hAnsi": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "individual rFonts @hAnsi slot readback. On Add/Set use the unified `font.latin` key (which writes both ascii + hAnsi).", + "readback": "font family name", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/sdt.json b/schemas/help/docx/sdt.json new file mode 100644 index 000000000..bb7364198 --- /dev/null +++ b/schemas/help/docx/sdt.json @@ -0,0 +1,66 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "sdt", + "parent": "body|paragraph", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/sdt[N]", "/body/p[N]/sdt[M]"] + }, + "note": "Aliases: contentcontrol. Structured document tags — inline or block. Block-level SDT wraps paragraphs; inline SDT wraps runs.", + "properties": { + "type": { + "type": "enum", + "description": "SDT variant. Only text/richtext/dropdown/combobox/date are supported at add-time. picture/checkbox are not implemented — create those in Word and edit via CLI. Type cannot be changed after creation.", + "values": ["text", "richtext", "dropdown", "combobox", "date"], + "add": true, "set": false, "get": true, + "examples": ["--prop type=text", "--prop type=dropdown"], + "readback": "type descriptor", + "enforcement": "report" + }, + "tag": { + "type": "string", + "description": "machine-readable tag for data-binding.", + "add": true, "set": true, "get": true, + "examples": ["--prop tag=customerName"], + "readback": "Tag attribute", + "enforcement": "report" + }, + "alias": { + "type": "string", + "description": "human-readable display name shown in Word.", + "add": true, "set": true, "get": true, + "examples": ["--prop alias=\"Customer Name\""], + "readback": "Alias attribute", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "placeholder/initial content.", + "add": true, "set": true, "get": true, + "examples": ["--prop text=\"[Enter name]\""], + "readback": "concatenated text", + "enforcement": "report" + }, + "id": { + "type": "number", + "description": "OOXML SdtId value; source of @sdtId in stable path /sdt[@sdtId=N].", + "add": false, "set": false, "get": true, + "readback": "integer", + "enforcement": "report" + }, + "editable": { + "type": "bool", + "description": "false when SdtContentLockingValues.SdtContentLocked is set on this content control.", + "add": false, "set": false, "get": true, + "readback": "true | false", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/section.json b/schemas/help/docx/section.json new file mode 100644 index 000000000..0c0f2fa20 --- /dev/null +++ b/schemas/help/docx/section.json @@ -0,0 +1,184 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "section", + "parent": "body", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/section[N]", "/body/sectPr[N]"] + }, + "note": "Sections are section-break paragraphs carrying SectionProperties. Canonical length readback is cm (via FormatTwipsToCm). Lenient length input (twips int, or 2cm/0.5in/24pt via ParseTwips).", + "properties": { + "type": { + "type": "enum", + "description": "section break type. Only applies to mid-document sections at /section[N]; the body-level path / refers to the final section which has no break type, and Set rejects 'type'/'break' there with an actionable error pointing at /section[N].", + "values": ["nextPage", "continuous", "evenPage", "oddPage", "nextColumn"], + "aliases": { + "nextPage": ["next", "nextpage", "newPage", "newpage", "page"], + "evenPage": ["even", "evenpage"], + "oddPage": ["odd", "oddpage"], + "nextColumn": ["column", "nextcolumn"] + }, + "propAliases": ["break"], + "add": true, "set": true, "get": true, + "examples": ["--prop type=nextPage", "--prop break=newPage"], + "readback": "one of values (innerText)", + "enforcement": "strict" + }, + "pageWidth": { + "type": "length", + "aliases": ["pagewidth", "width"], + "add": true, "set": true, "get": true, + "examples": ["--prop pageWidth=21cm"], + "readback": "unit-qualified cm (e.g. \"21cm\")", + "enforcement": "strict" + }, + "pageHeight": { + "type": "length", + "aliases": ["pageheight", "height"], + "add": true, "set": true, "get": true, + "examples": ["--prop pageHeight=29.7cm"], + "readback": "unit-qualified cm (e.g. \"29.7cm\")", + "enforcement": "strict" + }, + "orientation": { + "type": "enum", + "values": ["portrait", "landscape"], + "add": true, "set": true, "get": true, + "examples": ["--prop orientation=landscape"], + "readback": "innerText of PageOrientationValues", + "enforcement": "strict" + }, + "marginTop": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop marginTop=2.5cm"], + "readback": "unit-qualified cm", + "enforcement": "strict" + }, + "marginBottom": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop marginBottom=2.5cm"], + "readback": "unit-qualified cm", + "enforcement": "strict" + }, + "marginLeft": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop marginLeft=3cm"], + "readback": "unit-qualified cm", + "enforcement": "strict" + }, + "marginRight": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop marginRight=3cm"], + "readback": "unit-qualified cm", + "enforcement": "strict" + }, + "columns": { + "type": "int", + "description": "number of text columns. Add accepts combined form \"N\" or \"N,SPACE\" (e.g. \"2,1cm\"); separate space override via alias \"columns.space\".", + "aliases": ["columns.count"], + "add": true, "set": true, "get": true, + "examples": ["--prop columns=2", "--prop columns=2,1cm"], + "readback": "integer column count", + "enforcement": "strict" + }, + "columnSpace": { + "type": "length", + "description": "space between columns. Canonical key. Legacy alias 'columns.space' still accepted on Add/Set.", + "aliases": ["columns.space"], + "add": true, "set": true, "get": true, + "examples": ["--prop columnSpace=1cm"], + "readback": "unit-qualified cm", + "enforcement": "strict" + }, + "titlePage": { + "type": "bool", + "description": "enable distinct first-page header/footer for the section (writes ).", + "aliases": ["titlepage", "titlepg"], + "add": true, "set": true, "get": true, + "examples": ["--prop titlePage=true"], + "readback": "true when is present", + "enforcement": "strict" + }, + "pageNumFmt": { + "type": "enum", + "description": "page-number numeric format (writes w:pgNumType/@w:fmt). Common: decimal / lowerRoman / upperRoman / lowerLetter / upperLetter. Locale-specific: hindiNumbers / hindiVowels / arabicAlpha / arabicAbjad / thaiCounting / chineseCounting / japaneseCounting / koreanCounting / ideographDigital. Use 'hindiNumbers' for Indic-Arabic numerals (٠١٢٣) common in Arabic documents.", + "values": ["decimal", "lowerRoman", "upperRoman", "lowerLetter", "upperLetter", "hindiNumbers", "hindiVowels", "hindiConsonants", "hindiCounting", "arabicAlpha", "arabicAbjad", "thaiNumbers", "thaiLetters", "thaiCounting", "chineseCounting", "chineseCountingThousand", "chineseLegalSimplified", "japaneseCounting", "japaneseLegal", "japaneseDigitalTen", "koreanCounting", "koreanLegal", "koreanDigital", "ideographDigital", "ideographTraditional", "ideographZodiac", "none"], + "aliases": ["pagenumfmt", "pagenumberformat", "pagenumberfmt"], + "add": true, "set": true, "get": true, + "examples": ["--prop pageNumFmt=lowerRoman", "--prop pageNumFmt=hindiNumbers"], + "readback": "innerText of NumberFormatValues", + "enforcement": "strict" + }, + "direction": { + "type": "enum", + "values": ["ltr", "rtl"], + "description": "section reading direction (writes on sectPr). Flips page side, header/footer anchors, and gutter for Arabic / Hebrew documents. Apply at section level alongside paragraph-level direction for a fully RTL document.", + "aliases": ["dir", "bidi"], + "add": true, "set": true, "get": true, + "examples": ["--prop direction=rtl"], + "readback": "rtl (only emitted when sectPr/ is present)", + "enforcement": "strict" + }, + "rtlGutter": { + "type": "bool", + "description": "places the binding gutter on the right side (writes on sectPr). Used together with direction=rtl for Arabic/Hebrew layouts.", + "aliases": ["rtlgutter"], + "add": true, "set": true, "get": true, + "examples": ["--prop rtlGutter=true"], + "readback": "true (only emitted when sectPr/ is present)", + "enforcement": "report" + }, + "pageStart": { + "type": "int", + "description": "starting page number for the section (writes w:pgNumType/@w:start). Use 'none'/'off' to clear.", + "aliases": ["pagestart", "pagenumberstart", "pagenumstart"], + "add": true, "set": true, "get": true, + "examples": ["--prop pageStart=1", "--prop pageStart=none"], + "readback": "integer start value", + "enforcement": "strict" + }, + "lineNumbers": { + "type": "enum", + "values": ["continuous", "restartPage", "restartSection"], + "aliases": { + "restartPage": ["page"], + "restartSection": ["section"] + }, + "add": true, "set": true, "get": true, + "examples": ["--prop lineNumbers=continuous"], + "readback": "one of values", + "enforcement": "strict" + }, + "lineNumberCountBy": { + "type": "int", + "description": "line numbering interval (every Nth line gets a number). Companion to lineNumbers; only emitted when > 1.", + "aliases": ["linenumbercountby"], + "add": true, "set": true, "get": true, + "examples": ["--prop lineNumbers=continuous --prop lineNumberCountBy=5"], + "readback": "integer", + "enforcement": "strict" + }, + "headerRef": { "type":"string", "add":false, "set":false, "get":true, "description":"path to primary (default) header part. Convenience shortcut equal to headerRef.default when present.", "readback":"OOXML part path", "enforcement":"report" }, + "headerRef.default": { "type":"string", "add":false, "set":false, "get":true, "description":"path to default-type header part.", "readback":"OOXML part path", "enforcement":"report" }, + "headerRef.first": { "type":"string", "add":false, "set":false, "get":true, "description":"path to first-page-only header part.", "readback":"OOXML part path", "enforcement":"report" }, + "headerRef.even": { "type":"string", "add":false, "set":false, "get":true, "description":"path to even-page header part.", "readback":"OOXML part path", "enforcement":"report" }, + "footerRef": { "type":"string", "add":false, "set":false, "get":true, "description":"path to primary (default) footer part. Convenience shortcut equal to footerRef.default when present.", "readback":"OOXML part path", "enforcement":"report" }, + "footerRef.default": { "type":"string", "add":false, "set":false, "get":true, "description":"path to default-type footer part.", "readback":"OOXML part path", "enforcement":"report" }, + "footerRef.first": { "type":"string", "add":false, "set":false, "get":true, "description":"path to first-page-only footer part.", "readback":"OOXML part path", "enforcement":"report" }, + "footerRef.even": { "type":"string", "add":false, "set":false, "get":true, "description":"path to even-page footer part.", "readback":"OOXML part path", "enforcement":"report" }, + "colSpaces": { "type":"string", "add":false, "set":false, "get":true, "description":"per-column space overrides — comma-separated EMU/twips values, one per column. Surfaces when columns carry individual @space attrs.", "readback":"comma-separated integer twips", "enforcement":"report" }, + "columns.equalWidth":{ "type":"bool", "add":false, "set":false, "get":true, "description":"sectPr cols @equalWidth flag — true when all columns share the same width.", "readback":"true|false", "enforcement":"report" }, + "columns.separator": { "type":"bool", "add":false, "set":false, "get":true, "description":"sectPr cols @sep flag — vertical separator line drawn between columns.", "readback":"true when set", "enforcement":"report" } + } +} diff --git a/schemas/help/docx/style.json b/schemas/help/docx/style.json new file mode 100644 index 000000000..984c110b1 --- /dev/null +++ b/schemas/help/docx/style.json @@ -0,0 +1,848 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "style", + "parent": "styles", + "addParent": "/styles", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": [ + "/styles/StyleId" + ] + }, + "note": "Style type defaults to paragraph. 'id' must be unique in styles.xml; duplicate id rejected if explicit, else auto-suffixed. Built-in ids (Normal, Heading1..9, Title, Subtitle, Quote, IntenseQuote, ListParagraph, NoSpacing, TOCHeading) bypass the customStyle=true flag. Path forms /style[@name=NAME] and /style[N] are NOT supported — only /styles/StyleId resolves; navigation does not handle a bare 'style' top-level segment.", + "properties": { + "id": { + "type": "string", + "description": "w:styleId (unique, immutable identity). Aliases fall through to 'name' when 'id' is omitted. Renaming after Add would require rewriting every paragraph/run/basedOn reference in the document; not supported.", + "aliases": [ + "styleId", + "styleid" + ], + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop id=MyAccent", + "--prop styleId=MyAccent" + ], + "readback": "StyleId value", + "enforcement": "strict" + }, + "name": { + "type": "string", + "description": "display name. Defaults to 'id' when omitted.", + "aliases": [ + "styleName", + "stylename" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop name=\"My Accent\"", + "--prop styleName=\"My Accent\"" + ], + "readback": "StyleName.Val", + "enforcement": "report" + }, + "type": { + "type": "enum", + "values": [ + "paragraph", + "character", + "table", + "numbering" + ], + "aliases": { + "character": [ + "char" + ], + "paragraph": [ + "para" + ] + }, + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop type=paragraph" + ], + "readback": "one of values (innerText of StyleValues)", + "enforcement": "report", + "note": "Style type is fixed at creation — changing a style's type after Add would orphan every paragraph/run that already references it. Recreate the style if you need a different type." + }, + "basedOn": { + "type": "string", + "description": "parent style id to inherit from. Must be an existing w:styleId (not display name). Inherited properties are overridden by properties defined on this style.", + "aliases": [ + "basedon" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop basedOn=Normal" + ], + "readback": "BasedOn.Val", + "enforcement": "report" + }, + "basedOn.path": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "resolved path to the parent style node (get-only). Shortcut: use basedOn to Set.", + "readback": "/styles/{styleId}", + "enforcement": "report" + }, + "next": { + "type": "string", + "description": "next-paragraph style id.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop next=Normal" + ], + "readback": "NextParagraphStyle.Val", + "enforcement": "report" + }, + "linked": { + "type": "string", + "aliases": [ + "link" + ], + "description": "linked-style pair: a paragraph style references its companion character style (or vice versa). Word UI: 'Linked (paragraph and character)'. Value is the partner styleId; must exist in styles.xml.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop linked=MyCharStyle" + ], + "readback": "LinkedStyle.Val", + "enforcement": "report" + }, + "align": { + "type": "enum", + "values": [ + "left", + "center", + "right", + "justify", + "both", + "distribute" + ], + "aliases": [ + "alignment" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop align=center" + ], + "readback": "one of values", + "enforcement": "report" + }, + "spaceBefore": { + "type": "length", + "aliases": [ + "spacebefore" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop spaceBefore=12pt" + ], + "readback": "unit-qualified", + "enforcement": "report" + }, + "spaceAfter": { + "type": "length", + "aliases": [ + "spaceafter" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop spaceAfter=6pt" + ], + "readback": "unit-qualified", + "enforcement": "report" + }, + "font": { + "type": "string", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font=\"Calibri\"" + ], + "readback": "font name", + "enforcement": "report" + }, + "size": { + "type": "font-size", + "description": "font size. Accepts bare number or pt-suffixed.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop size=14" + ], + "readback": "unit-qualified pt", + "enforcement": "report" + }, + "bold": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop bold=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "italic": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop italic=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "color": { + "type": "color", + "description": "font color. Accepts #RRGGBB, RRGGBB, named colors (red, blue…), rgb(r,g,b), or 3-char shorthand (F00).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop color=#FF0000" + ], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "underline": { + "type": "string", + "description": "underline style (true/false, single, double, thick, dotted, dash, wavy, none, ...). Applied to the style's rPr.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop underline=single", + "--prop underline=double" + ], + "readback": "underline style or true/false", + "enforcement": "report" + }, + "strike": { + "type": "bool", + "description": "single-line strikethrough on the style's rPr.", + "aliases": [ + "strikethrough" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop strike=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "dstrike": { + "type": "bool", + "description": "double-line strikethrough on the style's rPr.", + "aliases": [ + "doublestrike" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop dstrike=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "highlight": { + "type": "string", + "description": "highlight color (yellow, green, cyan, magenta, blue, red, darkBlue, darkCyan, darkGreen, darkMagenta, darkRed, darkYellow, darkGray, lightGray, black, white, none).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop highlight=yellow" + ], + "readback": "highlight color name", + "enforcement": "report" + }, + "caps": { + "type": "bool", + "description": "all-caps display on the style's rPr.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop caps=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "smallCaps": { + "type": "bool", + "description": "small-caps display on the style's rPr.", + "aliases": [ + "smallcaps" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop smallCaps=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "vanish": { + "type": "bool", + "description": "hidden text on the style's rPr.", + "aliases": [ + "hidden" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop vanish=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "rtl": { + "type": "bool", + "description": "right-to-left run layout on the style's rPr.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop rtl=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "vertAlign": { + "type": "enum", + "values": [ + "superscript", + "subscript", + "baseline" + ], + "description": "vertical text alignment (superscript/subscript) on the style's rPr.", + "aliases": [ + "vertalign", + "verticalAlign" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop vertAlign=superscript" + ], + "readback": "one of values", + "enforcement": "report" + }, + "charSpacing": { + "type": "length", + "description": "character spacing (letter-spacing) on the style's rPr.", + "aliases": [ + "charspacing", + "letterSpacing", + "letterspacing" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop charSpacing=2pt" + ], + "readback": "unit-qualified pt", + "enforcement": "report" + }, + "shading": { + "type": "color", + "description": "background shading fill color on the style's rPr (or pPr for paragraph styles).", + "aliases": [ + "shd" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop shading=#FFFF00" + ], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "lineSpacing": { + "type": "string", + "description": "line spacing — multiplier (1.5x, 150%) or fixed (18pt). Applied to the style's pPr.", + "aliases": [ + "linespacing" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lineSpacing=1.5x", + "--prop lineSpacing=18pt" + ], + "readback": "\"x\" or \"pt\"", + "enforcement": "report" + }, + "lineRule": { + "type": "enum", + "description": "line spacing rule paired with lineSpacing. 'auto' = multiplier, 'exact' = exact fixed height, 'atLeast' = minimum height (lines may grow to fit tall content). Applied to the style's pPr.", + "values": [ + "auto", + "exact", + "atLeast" + ], + "aliases": [ + "linerule" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lineSpacing=14pt --prop lineRule=atLeast" + ], + "readback": "auto | exact | atLeast", + "enforcement": "report" + }, + "contextualSpacing": { + "type": "bool", + "description": "suppress spacing between paragraphs of the same style.", + "aliases": [ + "contextualspacing" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop contextualSpacing=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "outlineLvl": { + "type": "int", + "description": "outline level (0-9, 0=Heading 1). Drives TOC and Navigator. Applied to the style's pPr.", + "aliases": [ + "outlinelvl", + "outlineLevel", + "outlinelevel" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop outlineLvl=0" + ], + "readback": "integer 0-9", + "enforcement": "report" + }, + "kinsoku": { + "type": "bool", + "description": "kinsoku (CJK line-break rules) toggle. Applied to the style's pPr.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop kinsoku=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "snapToGrid": { + "type": "bool", + "description": "snap to document grid for CJK layout. Applied to the style's pPr. Add/Set only — Get does not surface this back today.", + "aliases": [ + "snaptogrid" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop snapToGrid=false" + ], + "readback": "n/a", + "enforcement": "report" + }, + "wordWrap": { + "type": "bool", + "description": "allow word-break for non-CJK text inside CJK lines. Applied to the style's pPr. Add/Set only — Get does not surface this back today.", + "aliases": [ + "wordwrap" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop wordWrap=true" + ], + "readback": "n/a", + "enforcement": "report" + }, + "autoSpaceDE": { + "type": "bool", + "description": "auto spacing between East-Asian and Latin text. Applied to the style's pPr.", + "aliases": [ + "autospacede" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop autoSpaceDE=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "autoSpaceDN": { + "type": "bool", + "description": "auto spacing between East-Asian text and numbers. Applied to the style's pPr.", + "aliases": [ + "autospacedn" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop autoSpaceDN=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "bidi": { + "type": "bool", + "description": "right-to-left paragraph direction. Applied to the style's pPr.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop bidi=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "direction": { + "type": "enum", + "values": [ + "rtl", + "ltr" + ], + "aliases": [ + "dir" + ], + "description": "Paragraph reading direction (Arabic / Hebrew). 'rtl' writes on the style pPr; equivalent to bidi=true in canonical form.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop direction=rtl" + ], + "readback": "rtl | ltr", + "enforcement": "report" + }, + "overflowPunct": { + "type": "bool", + "description": "allow punctuation to hang outside the text margin (CJK). Applied to the style's pPr.", + "aliases": [ + "overflowpunct" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop overflowPunct=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "topLinePunct": { + "type": "bool", + "description": "compress punctuation at the start of a line (CJK). Applied to the style's pPr. Add/Set only — Get does not surface this back today.", + "aliases": [ + "toplinepunct" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop topLinePunct=true" + ], + "readback": "n/a", + "enforcement": "report" + }, + "suppressAutoHyphens": { + "type": "bool", + "description": "disable automatic hyphenation in this style. Add/Set only — Get does not surface this back today.", + "aliases": [ + "suppressautohyphens" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop suppressAutoHyphens=true" + ], + "readback": "n/a", + "enforcement": "report" + }, + "suppressLineNumbers": { + "type": "bool", + "description": "exclude this paragraph style from line numbering. Add/Set only — Get does not surface this back today.", + "aliases": [ + "suppresslinenumbers" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop suppressLineNumbers=true" + ], + "readback": "n/a", + "enforcement": "report" + }, + "keepNext": { + "type": "bool", + "description": "keep this paragraph on the same page as the next.", + "aliases": [ + "keepnext" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop keepNext=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "keepLines": { + "type": "bool", + "description": "keep all lines of this paragraph together on one page.", + "aliases": [ + "keeplines" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop keepLines=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "pageBreakBefore": { + "type": "bool", + "description": "force a page break before each paragraph using this style.", + "aliases": [ + "pagebreakbefore" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop pageBreakBefore=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "widowControl": { + "type": "bool", + "description": "prevent widows and orphans (single isolated lines).", + "aliases": [ + "widowcontrol" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop widowControl=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "pbdr": { + "type": "string", + "description": "paragraph border. Sub-keys: pbdr.top / pbdr.bottom / pbdr.left / pbdr.right / pbdr.between / pbdr.bar / pbdr.all. Value form: 'style:size:color' (e.g. 'single:6:#FF0000'). Set-only — Get does not surface paragraph borders on the style today.", + "aliases": [ + "border" + ], + "add": false, + "set": true, + "get": false, + "examples": [ + "--prop pbdr.bottom=single:6:#FF0000", + "--prop pbdr.all=single:4:auto" + ], + "readback": "n/a", + "enforcement": "report" + }, + "numId": { + "type": "int", + "description": "numbering instance ID this style references. Paragraphs using --prop style= inherit numbering through ResolveNumPrFromStyle without their own numPr — the canonical multi-level outline pattern (Heading1..9). Requires the numId to already exist in /numbering.", + "aliases": [ + "numid" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop numId=3" + ], + "readback": "integer numId on style/pPr/numPr", + "enforcement": "report" + }, + "ilvl": { + "type": "int", + "description": "list level (0-8) for the style-borne numPr.", + "aliases": [ + "numLevel", + "numlevel" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop ilvl=0" + ], + "readback": "integer 0-8", + "enforcement": "report" + }, + "effective.alignment": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "resolved paragraph alignment after walking basedOn → linked → docDefaults.", + "readback": "alignment token (left|center|right|both|distribute)", + "enforcement": "report" + }, + "effective.alignment.src": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "source pointer for effective.alignment (style id chain).", + "readback": "style id or `docDefaults`", + "enforcement": "report" + }, + "effective.direction": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "resolved paragraph reading direction (rtl|ltr).", + "readback": "`rtl` | `ltr`", + "enforcement": "report" + }, + "effective.direction.src": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "source pointer for effective.direction.", + "readback": "style id or `docDefaults`", + "enforcement": "report" + }, + "effective.highlight": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "resolved highlight color name (yellow|green|cyan|...) inherited from the style chain.", + "readback": "highlight token", + "enforcement": "report" + }, + "effective.lineSpacing": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "resolved line spacing (`x` or `pt`).", + "readback": "unit-qualified spacing", + "enforcement": "report" + }, + "effective.lineSpacing.src": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "source pointer for effective.lineSpacing.", + "readback": "style id or `docDefaults`", + "enforcement": "report" + }, + "effective.spaceBefore": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "resolved space-before (unit-qualified).", + "readback": "unit-qualified length", + "enforcement": "report" + }, + "effective.spaceBefore.src": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "source pointer for effective.spaceBefore.", + "readback": "style id or `docDefaults`", + "enforcement": "report" + }, + "effective.spaceAfter": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "resolved space-after (unit-qualified).", + "readback": "unit-qualified length", + "enforcement": "report" + }, + "effective.spaceAfter.src": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "source pointer for effective.spaceAfter.", + "readback": "style id or `docDefaults`", + "enforcement": "report" + }, + "effective.strike": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "true when strike-through is inherited from the style chain.", + "readback": "true|false", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/styles.json b/schemas/help/docx/styles.json new file mode 100644 index 000000000..3ae8051ae --- /dev/null +++ b/schemas/help/docx/styles.json @@ -0,0 +1,24 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "styles", + "parent": "document", + "container": true, + "operations": { + "add": true, + "set": false, + "get": true, + "query": true, + "remove": false + }, + "paths": { + "positional": ["/styles"] + }, + "note": "StyleDefinitionsPart container. Add new styles here (see docx/style.json). Individual styles addressed by id: /styles/StyleId.", + "properties": { + "count": { "type":"number", "add":false, "set":false, "get":true, "description":"total number of style definitions in styles.xml.", "readback":"integer style count", "enforcement":"report" } + }, + "children": [ + { "element": "style", "pathSegment": "{StyleId}", "cardinality": "0..n" } + ] +} diff --git a/schemas/help/docx/table-cell.json b/schemas/help/docx/table-cell.json new file mode 100644 index 000000000..17eeeffb5 --- /dev/null +++ b/schemas/help/docx/table-cell.json @@ -0,0 +1,368 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "cell", + "elementAliases": ["tc"], + "parent": "row", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/body/tbl[N]/tr[R]/tc[C]" + ] + }, + "note": "Only 'text' and 'width' are honored at Add time; every other property is applied via Set after the cell exists. Run-level formatting (font/size/bold/italic/color/highlight/underline/strike) is written to every run in every paragraph in the cell — and to ParagraphMarkRunProperties when the cell has no runs yet. Border value format is STYLE[;SIZE[;COLOR[;SPACE]]], e.g. 'single;4;FF0000'.", + "extends": "_shared/table-cell", + "properties": { + "width": { + "type": "int", + "description": "cell width in twips (Dxa).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop width=2500" + ], + "readback": "twips", + "enforcement": "report" + }, + "skipGridSync": { + "type": "bool", + "description": "suppress the per-cell tblGrid synchronization side effect that normally fires when `width` is set. Used by dump → batch replay to preserve the source table's authoritative colWidths when individual tcW values disagree with the gridCol widths (Word renders by tcW; tblGrid is a layout hint). Set-only.", + "add": false, + "set": true, + "get": false, + "examples": [ + "--prop skipGridSync=true" + ], + "readback": "n/a", + "enforcement": "report" + }, + "font": { + "type": "string", + "description": "font family applied to every run in every paragraph in the cell (set-only; apply after add).", + "aliases": [ + "fontname", + "fontFamily" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop font=\"Times New Roman\"" + ], + "readback": "from first run's RunFonts.Ascii", + "enforcement": "report" + }, + "size": { + "type": "font-size", + "description": "font size applied to every run in the cell. Accepts raw number (points), '14pt', '10.5pt' (set-only; apply after add).", + "aliases": [ + "fontsize" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop size=14pt", + "--prop size=10.5pt" + ], + "readback": "unit-qualified, e.g. \"14pt\"", + "enforcement": "report" + }, + "bold": { + "type": "bool", + "description": "bold applied to every run in the cell (set-only; apply after add).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop bold=true" + ], + "readback": "true | (absent)", + "enforcement": "report" + }, + "italic": { + "type": "bool", + "description": "italic applied to every run in the cell (set-only; apply after add).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop italic=true" + ], + "readback": "true | (absent)", + "enforcement": "report" + }, + "underline": { + "type": "enum", + "values": [ + "none", + "single", + "double", + "thick", + "dotted", + "dash", + "wave", + "words" + ], + "description": "underline style applied to every run in the cell (set-only; apply after add).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop underline=single", + "--prop underline=double" + ], + "readback": "one of values", + "enforcement": "report" + }, + "strike": { + "type": "bool", + "description": "strike-through applied to every run in the cell (set-only; apply after add).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop strike=true" + ], + "readback": "true | (absent)", + "enforcement": "report" + }, + "color": { + "type": "color", + "description": "run text color applied to every run in the cell (set-only; apply after add).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop color=FF0000", + "--prop color=#FF0000", + "--prop color=red" + ], + "readback": "#RRGGBB uppercase", + "enforcement": "report" + }, + "highlight": { + "type": "color", + "description": "text highlight color. Mapped to Word's named highlight palette (yellow, green, cyan, magenta, blue, red, darkBlue, …) (set-only; apply after add).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop highlight=yellow" + ], + "readback": "highlight palette name", + "enforcement": "report" + }, + "align": { + "type": "enum", + "values": [ + "left", + "center", + "right", + "justify", + "both", + "distribute" + ], + "description": "horizontal paragraph alignment applied to every paragraph in the cell (set-only; apply after add).", + "aliases": [ + "alignment" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop align=center" + ], + "readback": "one of values (from first paragraph)", + "enforcement": "report" + }, + "valign": { + "type": "enum", + "values": [ + "top", + "center", + "bottom" + ], + "description": "vertical alignment of cell contents (set-only; apply after add).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop valign=center" + ], + "readback": "one of values", + "enforcement": "report" + }, + "colspan": { + "type": "int", + "description": "number of grid columns this cell spans. Aliases: gridspan. Adjusts cell width to the sum of spanned grid columns and removes now-redundant trailing cells in the row (set-only; apply after add).", + "aliases": [ + "gridspan" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop colspan=2" + ], + "readback": "under key 'colspan' when > 1", + "enforcement": "report" + }, + "fitText": { + "type": "bool", + "description": "enable w:fitText on every run so text is squeezed to the cell width (set-only; apply after add).", + "aliases": [ + "fittext" + ], + "add": false, + "set": true, + "get": false, + "examples": [ + "--prop fitText=true" + ], + "readback": "n/a", + "enforcement": "report" + }, + "textDirection": { + "type": "enum", + "values": [ + "lrtb", + "btlr", + "tbrl", + "horizontal", + "vertical", + "vertical-rl", + "tbrl-r", + "lrtb-r", + "tblr-r" + ], + "description": "text flow direction inside the cell. Aliases: textdir (set-only; apply after add).", + "aliases": [ + "textdir" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop textDirection=btlr" + ], + "readback": "OpenXML enum inner text", + "enforcement": "report" + }, + "direction": { + "type": "enum", + "values": [ + "rtl", + "ltr" + ], + "aliases": [ + "dir", + "bidi" + ], + "description": "Reading direction (Arabic / Hebrew). 'rtl' writes on every cell paragraph, on each paragraph mark, and on every run; 'ltr' clears all three. Distinct from textDirection (which controls vertical/horizontal text flow inside the cell).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop direction=rtl" + ], + "readback": "rtl when set, key absent otherwise", + "enforcement": "report" + }, + "nowrap": { + "type": "bool", + "description": "disable text wrapping inside the cell (set-only; apply after add).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop nowrap=true" + ], + "readback": "true | (absent)", + "enforcement": "report" + }, + "padding.top": { + "type": "number", + "description": "top cell margin in twips (integer; 1 twip = 1/20 pt, e.g. 100 = 5pt). Raw integer only — no unit suffix.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop padding.top=100" + ], + "readback": "twips integer", + "enforcement": "report" + }, + "padding.bottom": { + "type": "number", + "description": "bottom cell margin in twips (integer; 1 twip = 1/20 pt, e.g. 100 = 5pt). Raw integer only — no unit suffix.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop padding.bottom=100" + ], + "readback": "twips integer", + "enforcement": "report" + }, + "padding.left": { + "type": "number", + "description": "left cell margin in twips (integer; 1 twip = 1/20 pt, e.g. 100 = 5pt). Raw integer only — no unit suffix.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop padding.left=100" + ], + "readback": "twips integer", + "enforcement": "report" + }, + "padding.right": { + "type": "number", + "description": "right cell margin in twips (integer; 1 twip = 1/20 pt, e.g. 100 = 5pt). Raw integer only — no unit suffix.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop padding.right=100" + ], + "readback": "twips integer", + "enforcement": "report" + }, + "vmerge": { + "type": "enum", + "values": ["restart", "continue"], + "description": "vertical merge marker (w:vMerge). 'restart' marks the top cell of a vertical span; 'continue' marks subsequent merged cells in the same column. Bare reads as 'continue'.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop vmerge=restart", + "--prop vmerge=continue" + ], + "readback": "restart|continue", + "enforcement": "report" + }, + "hmerge": { + "type": "enum", + "values": ["restart", "continue"], + "description": "horizontal merge marker (w:hMerge — legacy form). 'restart' marks the leading cell of a horizontal span; 'continue' marks subsequent merged cells. Most modern docs prefer gridSpan; hmerge is preserved for round-trip with files that already use it.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop hmerge=restart", + "--prop hmerge=continue" + ], + "readback": "restart|continue", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/table-column.json b/schemas/help/docx/table-column.json new file mode 100644 index 000000000..f6700b45a --- /dev/null +++ b/schemas/help/docx/table-column.json @@ -0,0 +1,36 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "column", + "elementAliases": ["col"], + "parent": "table", + "operations": { + "add": true, + "set": false, + "get": false, + "query": false, + "remove": true + }, + "paths": { + "positional": ["/body/tbl[N]/col[C]"] + }, + "note": "Virtual element — OOXML has no child of ; the path is synthesized from / + the per-row cell at column slot C. Same-table only for move/copy. Get/Set/Query at the column level are not supported (read width via /body/tbl[N] tblGrid or per-cell tcW). Insert is rejected when the column slot crosses a merged cell (gridSpan/vMerge) — unmerge first.", + "properties": { + "width": { + "type": "length", + "description": "column width in twips (or any twips-parseable length).", + "add": true, "set": false, "get": false, + "examples": ["--prop width=2400", "--prop width=3cm"], + "readback": "n/a (column-level Get not implemented; inspect tblGrid)", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "seed text inserted into every new cell of this column (one paragraph per cell).", + "add": true, "set": false, "get": false, + "examples": ["--prop text=Header"], + "readback": "not surfaced at column level", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/docx/table-row.json b/schemas/help/docx/table-row.json new file mode 100644 index 000000000..91112782a --- /dev/null +++ b/schemas/help/docx/table-row.json @@ -0,0 +1,56 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "row", + "elementAliases": ["tr"], + "parent": "table", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/body/tbl[N]/tr[R]" + ] + }, + "note": "Row column count defaults to the parent table grid. height uses AtLeast rule; height.exact forces Exact rule.", + "extends": "_shared/table-row", + "properties": { + "height.exact": { + "type": "length", + "description": "row height in twips (Exact rule, cannot grow). Add/Set only — Get does not surface a separate exact-height key; inspect `height.rule=exact` paired with `height` instead.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop height.exact=500" + ], + "readback": "n/a (inspect height + height.rule)", + "enforcement": "report" + }, + "header": { + "type": "bool", + "description": "repeat row as table header on every page.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop header=true" + ], + "readback": "true when header, key absent otherwise", + "enforcement": "report" + }, + "height.rule": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "row height rule readback — `exact` when the row enforces a fixed height, otherwise absent (auto/atLeast).", + "readback": "`exact` when set", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/table.json b/schemas/help/docx/table.json new file mode 100644 index 000000000..3cce67338 --- /dev/null +++ b/schemas/help/docx/table.json @@ -0,0 +1,146 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "table", + "elementAliases": ["tbl"], + "parent": "body", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/body/tbl[N]" + ] + }, + "note": "Tables default to Single/Size=4 borders on all sides. Length props use twips (raw int) or unit-qualified. Data can be seeded via 'data' (semicolon rows, comma cells) or per-cell 'r{R}c{C}' props.", + "children": [ + { + "element": "row", + "pathSegment": "tr", + "cardinality": "1..n" + }, + { + "element": "cell", + "pathSegment": "tc", + "cardinality": "1..n (per row)" + } + ], + "extends": [ + "_shared/table", + "_shared/table.docx-pptx" + ], + "properties": { + "colWidths": { + "type": "string", + "description": "comma-separated per-column widths in twips. Aliases: colwidths.", + "aliases": [ + "colwidths" + ], + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop colWidths=3000,2000,5000" + ], + "readback": "comma-separated column widths in OOXML units", + "enforcement": "strict" + }, + "direction": { + "type": "enum", + "values": [ + "rtl", + "ltr" + ], + "aliases": [ + "dir", + "bidi" + ], + "description": "Reading direction (Arabic / Hebrew). 'rtl' writes on tblPr (mirrors column order); 'ltr' clears it. Distinct from per-cell textDirection.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop direction=rtl" + ], + "readback": "rtl when set, key absent otherwise", + "enforcement": "report" + }, + "align": { + "type": "enum", + "values": [ + "left", + "center", + "right" + ], + "aliases": [ + "alignment" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop align=center" + ], + "readback": "one of values", + "enforcement": "report" + }, + "indent": { + "type": "int", + "description": "table indent in twips.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop indent=200" + ], + "readback": "twips", + "enforcement": "report" + }, + "cellSpacing": { + "type": "int", + "description": "space between cells in twips. Alias: cellspacing.", + "aliases": [ + "cellspacing" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop cellSpacing=40" + ], + "readback": "twips", + "enforcement": "report" + }, + "layout": { + "type": "enum", + "values": [ + "fixed", + "autofit" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop layout=fixed" + ], + "readback": "one of values", + "enforcement": "report" + }, + "padding": { + "type": "int", + "description": "default cell padding (all four sides) in twips. Add/Set only — Get does not surface the table-default cell margin today.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop padding=100" + ], + "readback": "n/a", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/toc.json b/schemas/help/docx/toc.json new file mode 100644 index 000000000..e60d36e16 --- /dev/null +++ b/schemas/help/docx/toc.json @@ -0,0 +1,52 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "toc", + "parent": "body|paragraph", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/toc", "/tableofcontents"] + }, + "note": "Aliases: tableofcontents. Inserts a TOC field (complex fldChar). Word rebuilds the rendered entries on open unless 'pre-render' is used.", + "properties": { + "levels": { + "type": "string", + "description": "heading range (e.g. '1-3').", + "add": true, "set": true, "get": true, + "examples": ["--prop levels=1-3"], + "readback": "levels string", + "enforcement": "report" + }, + "title": { + "type": "string", + "description": "optional caption above the TOC.", + "add": true, "set": true, "get": true, + "examples": ["--prop title=\"Contents\""], + "readback": "caption text", + "enforcement": "report" + }, + "hyperlinks": { + "type": "bool", + "description": "generate clickable links.", + "add": true, "set": true, "get": true, + "examples": ["--prop hyperlinks=true"], + "readback": "true/false", + "enforcement": "report" + }, + "pageNumbers": { + "type": "bool", + "description": "include page numbers in TOC entries (Add/Set use lowercase alias 'pagenumbers').", + "aliases": ["pagenumbers"], + "add": true, "set": true, "get": true, + "examples": ["--prop pageNumbers=false"], + "readback": "true if TOC includes page numbers", + "enforcement": "report" + } + } +} diff --git a/schemas/help/docx/watermark.json b/schemas/help/docx/watermark.json new file mode 100644 index 000000000..a40750f24 --- /dev/null +++ b/schemas/help/docx/watermark.json @@ -0,0 +1,90 @@ +{ + "$schema": "../_schema.json", + "format": "docx", + "element": "watermark", + "parent": "body", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/watermark"] + }, + "note": "Watermarks are inserted into the document header as VML/DrawingML shapes. Text or image variants supported.", + "properties": { + "text": { + "type": "string", + "description": "watermark text (text variant).", + "add": true, "set": true, "get": true, + "examples": ["--prop text=DRAFT"], + "readback": "text content", + "enforcement": "report" + }, + "image": { + "type": "string", + "description": "image source for image watermark. Aliases: src, path.", + "aliases": ["src", "path"], + "add": true, "set": true, "get": false, + "examples": ["--prop image=/path/to/logo.png"], + "readback": "n/a", + "enforcement": "report" + }, + "color": { + "type": "color", + "add": true, "set": true, "get": true, + "examples": ["--prop color=#C0C0C0"], + "readback": "#-prefixed hex", + "enforcement": "report" + }, + "font": { + "type": "string", + "add": true, "set": true, "get": true, + "examples": ["--prop font=Calibri"], + "readback": "font name", + "enforcement": "report" + }, + "rotation": { + "type": "int", + "description": "VML rotation in degrees (0-360). Defaults to 315 (diagonal). Accepts negative input: -45 is stored and read back as 315.", + "add": true, "set": true, "get": true, + "examples": ["--prop rotation=315", "--prop rotation=-45"], + "readback": "rotation degrees (0-360, e.g. 315)", + "enforcement": "report" + }, + "opacity": { + "type": "string", + "description": "opacity 0..1 float as string (e.g. 0.5). Verbatim VML attribute injection.", + "add": true, "set": true, "get": true, + "examples": ["--prop opacity=.5"], + "readback": "opacity value", + "enforcement": "report" + }, + "size": { + "type": "string", + "description": "font size for text watermark (pt). Default 1pt (auto-fit).", + "add": true, "set": true, "get": true, + "examples": ["--prop size=72pt"], + "readback": "pt-suffixed size", + "enforcement": "report" + }, + "width": { + "type": "string", + "description": "watermark shape width (pt/in/cm).", + "add": true, "set": true, "get": true, + "examples": ["--prop width=415pt"], + "readback": "shape width", + "enforcement": "report" + }, + "height": { + "type": "string", + "description": "watermark shape height (pt/in/cm).", + "add": true, "set": true, "get": true, + "examples": ["--prop height=207.5pt"], + "readback": "shape height", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/animation.json b/schemas/help/pptx/animation.json new file mode 100644 index 000000000..bc2622673 --- /dev/null +++ b/schemas/help/pptx/animation.json @@ -0,0 +1,138 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "animation", + "parent": ["shape", "chart"], + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/shape[M]/animation[K]", + "/slide[N]/chart[M]/animation[K]" + ] + }, + "note": "Animation attached to a shape or chart graphicFrame. Chart targets additionally write alongside the timing tree (see chartBuild). No composite 'animation' key on Get — each facet (effect, class, presetId, trigger, duration, …) is its own key. `direction` is consumed at Add only.", + "properties": { + "effect": { + "type": "enum", + "description": "animation preset. spin/grow/bold/wave require class=emphasis; appear/fade/fly/zoom/wipe/bounce/float/swivel/split/wheel/checkerboard/blinds/dissolve/flash/box/circle/diamond/plus/strips/wedge/random work for entrance and exit. Sixteen additional exit effects are backed by verbatim PowerPoint OOXML templates (anim primitives copied byte-for-byte from a PowerPoint-authored deck): contract, centerRevolve, collapse, floatOut, shrinkTurn, sinkDown, spinner, basicZoom, stretchy, boomerang, credits, curveDown, pinwheel, spiralOut, basicSwivel — these require class=exit. The plain 'float' effect is the Exciting-menu Float (preset 30); use 'floatOut' for the Moderate-menu Float Out (preset 42). Template effects ignore the duration prop (they keep PowerPoint's authored timing). (disappear is not supported — use class=exit + appear or fade.)", + "values": ["appear", "fade", "fly", "zoom", "wipe", "bounce", "float", "swivel", "split", "wheel", "checkerboard", "blinds", "dissolve", "flash", "box", "circle", "diamond", "plus", "strips", "wedge", "random", "spin", "grow", "wave", "bold", "contract", "centerRevolve", "collapse", "floatOut", "shrinkTurn", "sinkDown", "spinner", "basicZoom", "stretchy", "boomerang", "credits", "curveDown", "pinwheel", "spiralOut", "basicSwivel", "fillColor", "growShrink", "lineColor", "transparency", "complementaryColor", "complementaryColor2", "contrastingColor", "darken", "desaturate", "lighten", "objectColor", "pulse", "colorPulse", "teeter"], + "add": true, "set": true, "get": true, + "examples": ["--prop effect=fade", "--prop effect=spin --prop class=emphasis"], + "readback": "effect name", + "enforcement": "report" + }, + "class": { + "type": "enum", + "description": "animation category — entrance, exit, emphasis, or motion. spin/grow/wave only work with emphasis; motion needs path=.", + "values": ["entrance", "exit", "emphasis", "motion"], + "add": true, "set": true, "get": true, + "examples": ["--prop class=entrance", "--prop class=motion --prop path=line"], + "readback": "entrance | exit | emphasis | motion", + "enforcement": "report" + }, + "path": { + "type": "enum", + "description": "Motion-path preset (only valid when class=motion). 'custom' requires d=. Direction-aware presets (line, arc) accept direction= for variants.", + "values": ["line", "arc", "circle", "diamond", "triangle", "square", "custom"], + "add": true, "set": true, "get": true, + "examples": ["--prop class=motion --prop path=line --prop direction=right", "--prop class=motion --prop path=circle"], + "readback": "preset name (line | arc | circle | diamond | triangle | square | custom)", + "enforcement": "report" + }, + "d": { + "type": "string", + "description": "Custom motion-path data (SVG-like; only valid when class=motion and path=custom). Coords are relative to slide (0..1). Must end with 'E' (auto-appended if missing).", + "add": true, "set": true, "get": true, + "examples": ["--prop class=motion --prop path=custom --prop d='M 0 0 L 0.5 0 E'"], + "readback": "raw OOXML animMotion path string, only when path=custom", + "enforcement": "report" + }, + "trigger": { + "type": "enum", + "values": ["onClick", "withPrevious", "afterPrevious"], + "add": true, "set": true, "get": true, + "examples": ["--prop trigger=onClick"], + "readback": "trigger mode", + "enforcement": "report" + }, + "duration": { + "type": "number", + "description": "Animation duration in milliseconds (integer, e.g. 500 = 0.5s).", + "aliases": ["dur"], + "add": true, "set": true, "get": true, + "examples": ["--prop duration=500", "--prop dur=2000"], + "readback": "duration in milliseconds", + "enforcement": "report" + }, + "delay": { + "type": "number", + "description": "Delay before starting in milliseconds (integer, e.g. 500 = 0.5s).", + "add": true, "set": true, "get": true, + "examples": ["--prop delay=200"], + "readback": "delay in milliseconds", + "enforcement": "report" + }, + "direction": { + "type": "string", + "description": "direction for directional effects (in/out/left/right/up/down).", + "add": true, "set": true, "get": false, + "examples": ["--prop direction=in"], + "readback": "packed into the 'animation' key value as 'effectName-class-direction-duration' (e.g. 'fly-entrance-left-500'); no standalone 'direction' key is emitted on Get", + "enforcement": "report" + }, + "repeat": { + "type": "string", + "description": "Number of times the animation repeats. Accepts a positive integer (e.g. 3) or the literal 'indefinite' to loop forever. Stored as OOXML @repeatCount (count*1000 or 'indefinite').", + "add": true, "set": true, "get": true, + "examples": ["--prop repeat=3", "--prop repeat=indefinite"], + "readback": "positive integer count, or the literal 'indefinite'", + "enforcement": "report" + }, + "restart": { + "type": "enum", + "description": "What happens when the trigger fires again after the animation has played. always = restart; whenNotActive = restart only if not currently playing; never = do nothing.", + "values": ["always", "whenNotActive", "never"], + "add": true, "set": true, "get": true, + "examples": ["--prop restart=always", "--prop restart=whenNotActive"], + "readback": "always | whenNotActive | never", + "enforcement": "report" + }, + "autoReverse": { + "type": "bool", + "description": "When true, the animation plays forward then in reverse, doubling its visible run. Maps to OOXML cTn @autoRev.", + "add": true, "set": true, "get": true, + "examples": ["--prop autoReverse=true"], + "readback": "true when @autoRev=1 is present, omitted otherwise", + "enforcement": "report" + }, + "presetId": { + "type": "number", + "add": false, "set": false, "get": true, + "description": "raw OOXML preset id for the animation effect. Emitted when the effect has a recognized preset.", + "readback": "integer", + "enforcement": "report" + }, + "easein": { "type":"number", "add":false, "set":false, "get":true, "description":"acceleration percentage (0..100) — fraction of the duration spent ramping up.", "readback":"integer percent", "enforcement":"report" }, + "easeout": { "type":"number", "add":false, "set":false, "get":true, "description":"deceleration percentage (0..100) — fraction of the duration spent ramping down.", "readback":"integer percent", "enforcement":"report" }, + "motionPath": { "type":"string", "add":false, "set":false, "get":true, "description":"motion-path SVG-like path string (animMotion @path) for path animations.", "readback":"OOXML animMotion path string", "enforcement":"report" }, + "chartBuild": { + "type": "enum", + "description": "Chart-internal build animation (only valid when the parent path is /slide[N]/chart[M]). Controls how chart elements appear under the entrance effect: asWhole (default — entire chart as one), series (one series at a time), category (one category at a time), seriesEl (each data point per series), categoryEl (each data point per category). Aliases accepted on input: bySeries, byCategory, bySeriesEl, byCategoryEl. Writes inside . chartBuild is chart-wide — setting it via any /slide[N]/chart[M]/animation[K] propagates to every animation on the chart.", + "values": ["asWhole", "series", "category", "seriesEl", "categoryEl"], + "aliases": ["bySeries", "byCategory", "bySeriesEl", "byCategoryEl"], + "add": true, "set": true, "get": true, + "examples": [ + "--prop \"effect=fade;class=entrance;chartBuild=byCategory\"", + "--prop \"effect=wipe;chartBuild=series;trigger=afterPrevious;duration=500\"" + ], + "readback": "asWhole | series | category | seriesEl | categoryEl", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/chart-axis.json b/schemas/help/pptx/chart-axis.json new file mode 100644 index 000000000..d390bb912 --- /dev/null +++ b/schemas/help/pptx/chart-axis.json @@ -0,0 +1,27 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "chart-axis", + "parent": "chart", + "operations": { + "add": false, + "set": true, + "get": true, + "remove": false + }, + "note": "Axes are created/destroyed implicitly by chartType changes — no direct Add/Remove; Set/Get only operates on existing axes. At chart-creation time, configure axes via the chart's axis* props (axismin/axismax/axistitle/axisfont/…); chart-axis covers post-creation only. Known gaps: `labelFont` writes the title run (not tick labels); `lineWidth`/`lineDash` Set on an axis path applies to all plot-area series — use chart-series for series-specific line styling.", + "addressing": { + "key": "role", + "pathForm": "/slide[N]/chart[N]/axis[@role=ROLE]", + "keyValues": [ + "category", + "value", + "value2", + "series" + ] + }, + "extends": [ + "_shared/chart-axis", + "_shared/chart-axis.pptx-xlsx" + ] +} diff --git a/schemas/help/pptx/chart-series.json b/schemas/help/pptx/chart-series.json new file mode 100644 index 000000000..e68eeb786 --- /dev/null +++ b/schemas/help/pptx/chart-series.json @@ -0,0 +1,37 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "chart-series", + "parent": "chart", + "operations": { + "add": true, + "set": true, + "get": true, + "remove": true + }, + "paths": { + "stable": [ + "/slide[N]/chart[@id=ID]/series[@id=ID]" + ], + "positional": [ + "/slide[N]/chart[N]/series[N]" + ] + }, + "note": "At Add time, series pass as dotted props on the parent chart (series1.name, series1.values, series1.color, series1.categories); this schema is per-series Set/Get after creation. Combo charts (mixed chartType / secondary axis) are unsupported — create separate charts. `lineStyle` is not a key (rejected as UNSUPPORTED — use lineWidth + lineDash).", + "extends": [ + "_shared/chart-series", + "_shared/chart-series.pptx-xlsx" + ], + "properties": { + "categories": { + "type": "string", + "description": "per-series category override; range reference only. pptx override: not supported on per-series Set (handler rejects categories on /chart[N]/series[N] paths). Set categories at chart level via `categories=...` instead.", + "add": true, + "set": false, + "get": true, + "examples": ["--prop series1.categories=\"Sheet1!$A$2:$A$5\""], + "readback": "range reference string", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/chart.json b/schemas/help/pptx/chart.json new file mode 100644 index 000000000..c98a65c55 --- /dev/null +++ b/schemas/help/pptx/chart.json @@ -0,0 +1,138 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "chart", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": [ + "/slide[N]/chart[@id=ID]" + ], + "positional": [ + "/slide[N]/chart[N]" + ] + }, + "note": "source of truth: Core/Chart/ChartHelper.cs ParseChartType() for the classic (c:chart) family, Core/Chart/ChartExBuilder.cs IsExtendedChartType() for the extended (cx:chart) family. Adding a new chartType value MUST update both the handler and this file in the same PR — contract tests enforce equivalence. Axis configuration: chart-level axis* props (axismin, axismax, axistitle, axisfont, ...) are Add-time only; for post-creation axis Set/Get use the chart-axis element.", + "children": [ + { + "element": "chart-title", + "pathSegment": "title", + "cardinality": "0..1" + }, + { + "element": "chart-legend", + "pathSegment": "legend", + "cardinality": "0..1" + }, + { + "element": "chart-plotArea", + "pathSegment": "plotArea", + "cardinality": "0..1" + }, + { + "element": "chart-axis", + "pathSegment": "axis", + "cardinality": "0..n", + "key": "role", + "keyValues": [ + "category", + "value", + "value2", + "series" + ], + "appliesWhen": { + "chartType": [ + "bar", + "column", + "line", + "area", + "scatter", + "bubble", + "radar", + "stock", + "combo" + ] + } + }, + { + "element": "chart-series", + "pathSegment": "series", + "cardinality": "1..n" + } + ], + "extends": [ + "_shared/chart", + "_shared/chart.docx-pptx", + "_shared/chart.pptx-xlsx" + ], + "properties": { + "direction": { + "type": "string", + "aliases": [ + "rtl" + ], + "add": false, + "set": true, + "get": false, + "examples": [ + "--prop direction=rtl" + ], + "readback": "rtl|ltr", + "description": "Chart-level reading direction. rtl stamps a:rtl=\"1\" on chartSpace c:txPr lvl1pPr so default text bodies (axis labels, data labels) render right-to-left for Arabic / Hebrew." + }, + "id": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "OOXML chart shape id; source of @id in the stable path /chart[@id=ID].", + "readback": "integer chart shape id", + "enforcement": "report" + }, + "zorder": { + "type": "number", + "description": "1-based z-order in slide shape tree. On add, positions the chart within the shape tree (1 = back). Post-creation reordering is not supported via Set; use Move/Swap on the chart graphic frame, or raw-set the spTree child order.", + "aliases": ["z-order", "order"], + "add": true, + "set": false, + "get": true, + "examples": ["--prop zorder=1"], + "readback": "1-based integer (1 = back)", + "enforcement": "report" + }, + "chartType": { + "type": "enum", + "values": [ + "bar", "column", "line", "pie", "doughnut", "area", + "scatter", "bubble", "radar", "stock", "combo", + "waterfall", "funnel", "treemap", "sunburst", + "boxWhisker", "histogram", "pareto" + ], + "description": "pptx override: chartType is consumed by AddChart (creation only). Switching chart type post-creation is not supported — rebuild the chart with the new type.", + "aliases": ["type"], + "add": true, + "set": false, + "get": true, + "examples": ["--prop chartType=column"], + "readback": "normalized chartType string", + "enforcement": "report" + }, + "name": { + "type": "string", + "add": true, + "set": false, + "get": true, + "description": "shape name (DocProperties.Name).", + "examples": [ + "--prop name=\"Sales Chart\"" + ], + "readback": "shape name string", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/comment.json b/schemas/help/pptx/comment.json new file mode 100644 index 000000000..1f99e6597 --- /dev/null +++ b/schemas/help/pptx/comment.json @@ -0,0 +1,72 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "comment", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/comment[M]" + ] + }, + "note": "Comments live in CommentsPart with an author list. Anchored at x/y EMU on the slide.", + "extends": [ + "_shared/comment", + "_shared/comment.docx-pptx" + ], + "properties": { + "index": { + "type": "int", + "description": "Per-author monotonic index, assigned by the engine.", + "add": false, + "set": false, + "get": true, + "readback": "comment index", + "enforcement": "report" + }, + "x": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop x=2cm" + ], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "y": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop y=2cm" + ], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "direction": { + "type": "string", + "aliases": [ + "dir", + "rtl" + ], + "description": "Reading direction for the comment text. rtl prepends U+200F (RIGHT-TO-LEFT MARK) so Arabic / Hebrew comments render with proper bidi context. p:cm has no native rtl attribute, so this is the standard pure-text convention.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop direction=rtl" + ], + "readback": "rtl|ltr", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/connector.json b/schemas/help/pptx/connector.json new file mode 100644 index 000000000..a212ce0e7 --- /dev/null +++ b/schemas/help/pptx/connector.json @@ -0,0 +1,143 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "connector", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slide[N]/connector[M]"] + }, + "note": "Aliases: connection. Straight / bent / curved connector lines. 'from' and 'to' can reference shape paths to auto-attach endpoints.", + "properties": { + "shape": { + "type": "enum", + "values": ["straight", "elbow", "curve"], + "description": "Connector geometry preset. Add/Set accept the short names (straight, elbow, curve) or OOXML full names (straightConnector1, bentConnector2, bentConnector3, curvedConnector2, curvedConnector3 — bent/curved 2-segment forms map to the 3-segment primitive). Get readback returns the OOXML full name.", + "add": true, "set": true, "get": true, + "examples": ["--prop shape=straight", "--prop shape=elbow", "--prop shape=curve"], + "readback": "OOXML preset full name (straightConnector1, bentConnector3, curvedConnector3)", + "enforcement": "report" + }, + "from": { + "type": "string", + "description": "start-point shape reference (Add/Set only). Accepts /slide[N]/shape[M] (positional) or /slide[N]/shape[@id=M] (as returned by 'query shape'). Reverse path resolution is not implemented.", + "add": true, "set": true, "get": false, + "examples": ["--prop from='/slide[1]/shape[1]'", "--prop from='/slide[1]/shape[@id=10001]'"], + "readback": "see startShape/endShape get-only properties for resolved endpoint shape ids", + "enforcement": "report" + }, + "to": { + "type": "string", + "description": "end-point shape reference (Add/Set only). Accepts /slide[N]/shape[M] (positional) or /slide[N]/shape[@id=M] (as returned by 'query shape'). Reverse path resolution is not implemented.", + "add": true, "set": true, "get": false, + "examples": ["--prop to='/slide[1]/shape[2]'", "--prop to='/slide[1]/shape[@id=10002]'"], + "readback": "see startShape/endShape get-only properties for resolved endpoint shape ids", + "enforcement": "report" + }, + "x": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop x=1in"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "y": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop y=1in"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "width": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop width=2in"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "height": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop height=1in"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "color": { + "type": "color", + "add": true, "set": true, "get": true, + "examples": ["--prop color=#000000"], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "lineWidth": { + "type": "length", + "add": true, "set": true, "get": true, + "aliases": ["linewidth", "line.width"], + "examples": ["--prop lineWidth=2pt"], + "readback": "pt-qualified string", + "enforcement": "report" + }, + "lineDash": { + "type": "enum", + "values": ["solid", "dot", "dash", "dashDot", "lgDash", "lgDashDot", "lgDashDotDot", "sysDot", "sysDash", "sysDashDot", "sysDashDotDot", "longdash", "longdashdot"], + "description": "outline dash pattern (drawingML prstDash). Get readback emits the canonical OOXML token (lgDash/lgDashDot/lgDashDotDot/sysDot/...); the long* aliases are accepted on Add/Set only. 'dashDotDot' has no native enum member; it is accepted as an alias for sysDashDotDot (Get readback returns sysDashDotDot).", + "add": true, "set": true, "get": true, + "aliases": ["linedash"], + "examples": ["--prop lineDash=dash", "--prop lineDash=lgDash", "--prop lineDash=sysDash"], + "readback": "canonical OOXML preset dash token (e.g. lgDash, sysDashDot)", + "enforcement": "report" + }, + "headEnd": { + "type": "enum", + "values": ["none", "triangle", "arrow", "stealth", "diamond", "oval"], + "add": true, "set": true, "get": true, + "aliases": ["headend"], + "examples": ["--prop headEnd=triangle"], + "readback": "OOXML LineEndValues token (canonical)", + "enforcement": "report" + }, + "tailEnd": { + "type": "enum", + "values": ["none", "triangle", "arrow", "stealth", "diamond", "oval"], + "add": true, "set": true, "get": true, + "aliases": ["tailend"], + "examples": ["--prop tailEnd=arrow"], + "readback": "OOXML LineEndValues token (canonical)", + "enforcement": "report" + }, + "id": { + "type": "number", + "description": "OOXML shape id; source of the @id in the stable path /connector[@id=ID].", + "add": false, "set": false, "get": true, + "readback": "integer shape id", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "connector name", + "add": true, "set": true, "get": true, + "examples": ["--prop name=\"Arrow1\""], + "readback": "plain string", + "enforcement": "report" + }, + "startShape": { "type":"number", "add":false, "set":false, "get":true, "description":"shape id of the start connection endpoint.", "readback":"integer shape id", "enforcement":"report" }, + "startIdx": { "type":"number", "add":false, "set":false, "get":true, "description":"connection point index on start shape (0-based; omitted when 0).", "readback":"integer", "enforcement":"report" }, + "endShape": { "type":"number", "add":false, "set":false, "get":true, "description":"shape id of the end connection endpoint.", "readback":"integer shape id", "enforcement":"report" }, + "endIdx": { "type":"number", "add":false, "set":false, "get":true, "description":"connection point index on end shape (0-based; omitted when 0).", "readback":"integer", "enforcement":"report" }, + "zorder": { + "type": "string", + "description": "1-based z-order in slide shape tree. Add accepts 'top'/'bottom'/'forward'/'backward'/integer (same vocabulary as shape/picture/group); Get returns the resolved integer position.", + "aliases": ["z-order", "order"], + "add": true, "set": false, "get": true, + "examples": ["--prop zorder=top", "--prop zorder=2"], + "readback": "integer (1-based position among content elements)", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/equation.json b/schemas/help/pptx/equation.json new file mode 100644 index 000000000..586738173 --- /dev/null +++ b/schemas/help/pptx/equation.json @@ -0,0 +1,81 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "equation", + "parent": "slide|shape", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/shape[M]/oMath[K]" + ] + }, + "note": "Aliases: formula, math. FormulaParser parses LaTeX-ish input. Adding a 'shape' or 'textbox' with 'formula' prop also routes here.", + "extends": "_shared/equation", + "properties": { + "formula": { + "type": "string", + "description": "math expression. Aliases: text.", + "aliases": [ + "text" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop formula=\"x^2 + y^2 = z^2\"" + ], + "readback": "LaTeX expression reconstructed from the tree (PowerPointHandler.NodeBuilder.cs emits Format[\"formula\"]). docx does not implement this readback yet — see _shared/equation.json.", + "enforcement": "report" + }, + "x": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop x=2cm" + ], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "y": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop y=2cm" + ], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "width": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop width=10cm" + ], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "height": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop height=3cm" + ], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/group.json b/schemas/help/pptx/group.json new file mode 100644 index 000000000..e717c5d1e --- /dev/null +++ b/schemas/help/pptx/group.json @@ -0,0 +1,72 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "group", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slide[N]/group[M]"] + }, + "note": "Groups existing shapes on a slide. 'shapes' takes comma-separated shape indices or DOM paths. Group bounding box auto-computed from member transforms. Shapes inside a group are addressable via /slide[N]/group[M]/shape[K] for direct Set/Get. zorder is emitted only when the group appears as a child in slide Query results, not via direct Get on /slide[N]/group[N]. This is a known C# Query/Get inconsistency.", + "properties": { + "shapes": { + "type": "string", + "description": "comma-separated shape indices (1,2,3) or paths (/slide[N]/shape[M] or /slide[N]/shape[@id=ID]). Required.", + "add": true, "set": false, "get": false, + "examples": ["--prop shapes=1,2"], + "readback": "n/a (structural)", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "group name. Defaults to 'Group N'.", + "add": true, "set": true, "get": true, + "examples": ["--prop name=\"Logos\""], + "readback": "name string", + "enforcement": "report" + }, + "zorder": { + "type": "number", + "description": "1-based z-order in slide shape tree. On add, positions the group within the shape tree (1 = back). Post-creation reordering is not supported via Set; use Move/Swap on the group, or raw-set the spTree child order.", + "aliases": ["z-order", "order"], + "add": true, "set": false, "get": true, + "examples": ["--prop zorder=1"], + "readback": "1-based integer (1 = back)", + "enforcement": "report" + }, + "x": { + "type": "length", + "description": "horizontal offset (group origin). Readback in cm via EmuConverter.", + "add": false, "set": false, "get": true, + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "y": { + "type": "length", + "description": "vertical offset.", + "add": false, "set": false, "get": true, + "readback": "length in cm", + "enforcement": "report" + }, + "width": { + "type": "length", + "description": "group bounding box width.", + "add": false, "set": false, "get": true, + "readback": "length in cm", + "enforcement": "report" + }, + "height": { + "type": "length", + "description": "group bounding box height.", + "add": false, "set": false, "get": true, + "readback": "length in cm", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/hyperlink.json b/schemas/help/pptx/hyperlink.json new file mode 100644 index 000000000..524fb286a --- /dev/null +++ b/schemas/help/pptx/hyperlink.json @@ -0,0 +1,50 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "hyperlink", + "parent": "shape|run", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/shape[M]/hyperlink", + "/slide[N]/shape[M]/p[K]/r[L]/hyperlink" + ] + }, + "note": "Aliases: link. Attached to a shape (shape-wide link) or to a run (inline link). Internal slide jumps use link=slide[N]; named actions use link=firstslide|lastslide|nextslide|previousslide|endshow.", + "extends": "_shared/hyperlink", + "properties": { + "link": { + "type": "string", + "description": "external URL or internal target. pptx Set/Get canonical key on shape/run is 'link'. Alias: url.", + "aliases": [ + "url" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop link=https://example.com" + ], + "readback": "URL string or internal target", + "enforcement": "report" + }, + "tooltip": { + "type": "string", + "description": "hover-text shown by PowerPoint when the link is moused over. Paired with 'link'; standalone tooltip changes are a no-op.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop link=https://example.com --prop tooltip=\"Open docs\"" + ], + "readback": "tooltip text", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/media.json b/schemas/help/pptx/media.json new file mode 100644 index 000000000..fa1f4ec91 --- /dev/null +++ b/schemas/help/pptx/media.json @@ -0,0 +1,106 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "media", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slide[N]/video[M]", "/slide[N]/audio[M]"] + }, + "note": "Aliases: video, audio. Video/audio inferred from extension when type=media. Poster image auto-generated when not supplied.", + "properties": { + "src": { + "type": "string", + "description": "media source — file path, URL, or data-URI; accepted on add/set only. Get does NOT surface this key (no Format[\"src\"] or Format[\"relId\"] is emitted for media).", + "aliases": ["path"], + "add": true, "set": true, "get": false, + "examples": ["--prop src=/path/to/video.mp4"], + "readback": "add-time only; not surfaced in Get.", + "enforcement": "report" + }, + "poster": { + "type": "string", + "description": "custom thumbnail image path.", + "add": true, "set": true, "get": false, + "examples": ["--prop poster=/path/to/thumb.png"], + "readback": "n/a", + "enforcement": "report" + }, + "x": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop x=1in"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "y": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop y=1in"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "width": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop width=4in"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "height": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop height=3in"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "volume": { + "type": "int", + "description": "playback volume 0-100.", + "add": true, "set": true, "get": true, + "examples": ["--prop volume=80"], + "readback": "volume percent", + "enforcement": "report" + }, + "autoPlay": { + "type": "bool", + "aliases": ["autoplay", "autoStart", "autostart"], + "add": true, "set": true, "get": true, + "examples": ["--prop autoPlay=true"], + "readback": "true/false", + "enforcement": "report" + }, + "loop": { + "type": "bool", + "description": "loop until stopped (repeats the clip indefinitely during playback).", + "add": true, "set": true, "get": true, + "examples": ["--prop loop=true"], + "readback": "true/false", + "enforcement": "report" + }, + "trimStart": { + "type": "string", + "description": "trim from media start. Accepts ms ('200'), seconds ('1.5s'), or 'hh:mm:ss.fff' ('00:00:01.500'); stored as ms-int. Alias: trimstart.", + "aliases": ["trimstart"], + "add": true, "set": true, "get": true, + "examples": ["--prop trimStart=1500", "--prop trimStart=1.5s", "--prop trimStart=00:00:01.500"], + "readback": "milliseconds (string)", + "enforcement": "report" + }, + "trimEnd": { + "type": "string", + "description": "trim from media end. Same input forms as trimStart; stored as ms-int. Alias: trimend.", + "aliases": ["trimend"], + "add": true, "set": true, "get": true, + "examples": ["--prop trimEnd=10000", "--prop trimEnd=10s", "--prop trimEnd=00:00:10.000"], + "readback": "milliseconds (string)", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/model3d.json b/schemas/help/pptx/model3d.json new file mode 100644 index 000000000..ea39a90a8 --- /dev/null +++ b/schemas/help/pptx/model3d.json @@ -0,0 +1,67 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "model3d", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slide[N]/model3d[M]"] + }, + "note": "Only .glb (glTF-Binary) accepted. Placeholder PNG auto-generated for non-3D-aware viewers. Defaults to 10cm × 10cm centered on the slide.", + "properties": { + "src": { + "type": "string", + "description": ".glb source (file path, URL, data-URI). Non-glb rejected. Accepted on add/set only; Get does NOT surface this key (no Format[\"src\"] or Format[\"relId\"] is emitted for model3d).", + "aliases": ["path"], + "add": true, "set": true, "get": false, + "examples": ["--prop src=/path/to/model.glb"], + "readback": "add-time only; not surfaced in Get.", + "enforcement": "report" + }, + "x": { + "type": "length", + "aliases": ["left"], + "add": true, "set": true, "get": true, + "examples": ["--prop x=2cm"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "y": { + "type": "length", + "aliases": ["top"], + "add": true, "set": true, "get": true, + "examples": ["--prop y=2cm"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "width": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop width=10cm"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "height": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop height=10cm"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "rotation": { + "type": "string", + "description": "Model rotation as comma-separated degrees \"ax,ay,az\" (X, Y, Z axes). Missing axes default to 0. Per-axis aliases rotx/roty/rotz also accepted on set.", + "aliases": ["rotx", "roty", "rotz"], + "add": true, "set": true, "get": true, + "examples": ["--prop rotation=30,45,0", "--prop rotx=30"], + "readback": "\"ax,ay,az\" degrees (e.g. \"30,45,0\")", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/moderncomment.json b/schemas/help/pptx/moderncomment.json new file mode 100644 index 000000000..e2fe7de83 --- /dev/null +++ b/schemas/help/pptx/moderncomment.json @@ -0,0 +1,90 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "modernComment", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/modernComment[K]", + "/slide[N]/modernComment[K]/reply[R]" + ] + }, + "note": "Modern p188 (Office 2018/8) threaded comments. Distinct OOXML element from the legacy /slide[N]/comment[M]. Top-level threads live in PowerPointCommentPart (/ppt/comments/…); authors live in the presentation-level PowerPointAuthorsPart. Replies are nested as /slide[N]/modernComment[K]/reply[R] and surface as children on Get of the top-level node. resolved is a thread-level state (top-level only). Removing a top-level path removes the whole thread, mirroring PowerPoint UI.", + "properties": { + "author": { + "type": "string", + "add": true, + "set": true, + "get": true, + "examples": ["--prop author=\"Alice\""], + "readback": "author name", + "enforcement": "report" + }, + "initials": { + "type": "string", + "description": "author initials. Defaults to derived from author name when omitted.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop initials=A"], + "readback": "initials", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "comment body (plain text — rich-text replies deferred).", + "add": true, + "set": true, + "get": true, + "examples": ["--prop text=\"Please review\""], + "readback": "comment body text", + "enforcement": "report" + }, + "resolved": { + "type": "bool", + "description": "Thread-level resolved state. Applies only to top-level comments; set on a reply path is rejected as unsupported.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop resolved=true"], + "readback": "bool (true/false)", + "enforcement": "report" + }, + "created": { + "type": "string", + "description": "ISO-8601 timestamp. Defaults to DateTime.UtcNow on add. Set accepts any ISO-8601 string; normalized to UTC `Z` form on readback.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop created=2026-01-15T10:30:00Z"], + "readback": "ISO-8601 UTC", + "enforcement": "report" + }, + "parent": { + "type": "string", + "description": "Path of an existing top-level modernComment to reply to. Add only — supplying parent= turns the new comment into a reply nested under that thread. Empty/missing creates a top-level comment.", + "add": true, + "set": false, + "get": true, + "examples": ["--prop parent='/slide[1]/modernComment[1]'"], + "readback": "parent comment path, or null for top-level", + "enforcement": "report" + }, + "id": { + "type": "string", + "description": "GUID identifier assigned by the engine on add.", + "add": false, + "set": false, + "get": true, + "readback": "GUID in {…} form", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/notes.json b/schemas/help/pptx/notes.json new file mode 100644 index 000000000..a959f0a0a --- /dev/null +++ b/schemas/help/pptx/notes.json @@ -0,0 +1,43 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "notes", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slide[N]/notes"] + }, + "note": "Speaker notes live in a NotesSlidePart paired with the slide. Add creates the part if absent; Set replaces text.", + "properties": { + "text": { + "type": "string", + "description": "notes body text.", + "add": true, "set": true, "get": true, + "examples": ["--prop text=\"Emphasize slide 3 data\""], + "readback": "notes text", + "enforcement": "report" + }, + "direction": { + "type": "enum", + "values": ["ltr", "rtl"], + "aliases": ["dir", "rtl"], + "description": "Reading direction for the notes body. Sets on every paragraph and rtlCol=\"1\" on the body shape's bodyPr. Required for Arabic / Hebrew speaker notes.", + "add": true, "set": true, "get": false, + "examples": ["--prop direction=rtl"], + "enforcement": "report" + }, + "lang": { + "type": "string", + "description": "BCP-47 language tag applied to every run in the notes body (a:rPr/@lang). Mirrors the shape Set vocabulary.", + "add": true, "set": true, "get": false, + "examples": ["--prop lang=ar-SA"], + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/ole.json b/schemas/help/pptx/ole.json new file mode 100644 index 000000000..661b10f59 --- /dev/null +++ b/schemas/help/pptx/ole.json @@ -0,0 +1,48 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "ole", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/ole[M]" + ] + }, + "note": "Aliases: oleobject, object, embed. Binary package + preview image. Position via x/y/width/height (EMU-parseable; readback in cm).", + "extends": [ + "_shared/ole", + "_shared/ole.docx-pptx", + "_shared/ole.pptx-xlsx" + ], + "properties": { + "x": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop x=2cm" + ], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "y": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop y=2cm" + ], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/paragraph.json b/schemas/help/pptx/paragraph.json new file mode 100644 index 000000000..b9d0c3207 --- /dev/null +++ b/schemas/help/pptx/paragraph.json @@ -0,0 +1,140 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "paragraph", + "elementAliases": ["p"], + "parent": "shape|placeholder", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/shape[M]/p[K]" + ] + }, + "note": "Aliases: para. Appends a:p to a shape's TextBody. Alignment uses pptx vocabulary (l/ctr/r/just); lineSpacing via SpacingConverter.", + "children": [ + { + "element": "run", + "pathSegment": "r", + "cardinality": "0..n" + } + ], + "extends": "_shared/paragraph", + "properties": { + "align": { + "type": "enum", + "values": [ + "left", + "center", + "right", + "justify" + ], + "aliases": [ + "alignment", + "halign" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop align=center" + ], + "readback": "canonical 'align'", + "enforcement": "report" + }, + "level": { + "type": "int", + "description": "list indent level 0-8.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop level=1" + ], + "readback": "indent level", + "enforcement": "report" + }, + "marginLeft": { + "type": "length", + "add": true, + "set": true, + "get": true, + "description": "left text margin (CT_TextParagraphProperties @marL).", + "readback": "unit-qualified EMU length", + "enforcement": "report" + }, + "marginRight": { + "type": "length", + "add": true, + "set": true, + "get": true, + "description": "right text margin (CT_TextParagraphProperties @marR).", + "readback": "unit-qualified EMU length", + "enforcement": "report" + }, + "lineRule": { + "type": "enum", + "add": false, + "set": false, + "get": false, + "description": "Not applicable to pptx. PowerPoint has no independent line-spacing rule — the rule is inferred from the lineSpacing value's format (e.g. '1.5x' / '150%' → percent rule, '18pt' → fixed-points rule). Override of _shared/paragraph which inherits the docx-style lineRule.", + "enforcement": "report" + }, + "bold": { + "type": "bool", + "description": "paragraph default run weight. Stored on the paragraph's defRPr (CT_TextCharacterProperties) and inherited by every run in the paragraph that does not override `bold`. Folded onto the paragraph by dump when every run shares the same value.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop bold=true"], + "readback": "true|false", + "enforcement": "report" + }, + "italic": { + "type": "bool", + "description": "paragraph default run italic. Stored on the paragraph's defRPr and inherited by every run that does not override `italic`. Folded onto the paragraph by dump when every run shares the same value.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop italic=true"], + "readback": "true|false", + "enforcement": "report" + }, + "color": { + "type": "color", + "description": "paragraph default run color. Stored on the paragraph's defRPr solidFill and inherited by every run that does not override `color`. Folded onto the paragraph by dump when every run shares the same value.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop color=FF0000"], + "readback": "canonical hex color", + "enforcement": "report" + }, + "size": { + "type": "length", + "description": "paragraph default run font size (points). Stored on the paragraph's defRPr (@sz, units of 1/100 pt) and inherited by every run that does not override `size`. Folded onto the paragraph by dump when every run shares the same value.", + "add": true, + "set": true, + "get": true, + "examples": ["--prop size=14pt"], + "readback": "unit-qualified points (e.g. \"14pt\")", + "enforcement": "report" + }, + "lang": { + "type": "string", + "description": "BCP-47 language tag stamped on the paragraph's defRPr (@lang). Folded onto the paragraph by dump when every run shares the same lang value (single-run collapse). See pptx/run.json for full semantics.", + "aliases": ["lang.latin"], + "add": true, + "set": true, + "get": true, + "examples": ["--prop lang=en-US"], + "readback": "BCP-47 tag as written", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/pptx/picture.json b/schemas/help/pptx/picture.json new file mode 100644 index 000000000..d61569126 --- /dev/null +++ b/schemas/help/pptx/picture.json @@ -0,0 +1,179 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "picture", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/picture[M]" + ] + }, + "note": "Aliases: image, img. 'src' (alias 'path') required. Source resolved by ImageSource — file path, URL, data-URI, raw bytes.", + "extends": [ + "_shared/picture", + "_shared/picture.docx-pptx", + "_shared/picture.pptx-xlsx" + ], + "properties": { + "mediaType": { + "type": "string", + "description": "logical media kind derived from VideoFromFile / AudioFromFile presence under the picture. One of `picture`, `video`, `audio`. Surfaces only via the `image`/`video`/`audio`/`media` selectors.", + "add": false, + "set": false, + "get": true, + "readback": "`picture` | `video` | `audio`", + "enforcement": "report" + }, + "x": { + "type": "length", + "description": "x offset in EMU/length form (e.g. 2cm). pptx absolute positioning.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop x=0", + "--prop x=1in" + ], + "readback": "length string (FormatEmu)", + "enforcement": "report" + }, + "y": { + "type": "length", + "description": "y offset in EMU/length form (e.g. 2cm). pptx absolute positioning.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop y=0", + "--prop y=1in" + ], + "readback": "length string (FormatEmu)", + "enforcement": "report" + }, + "width": { + "type": "length", + "description": "width — EMU/length form (e.g. 1.5cm). Required if not inferred from native ratio.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop width=5", + "--prop width=3in" + ], + "readback": "length string", + "enforcement": "report" + }, + "height": { + "type": "length", + "description": "height — EMU/length form (e.g. 1.5cm). Required if not inferred from native ratio.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop height=5", + "--prop height=2in" + ], + "readback": "length string", + "enforcement": "report" + }, + "rotation": { + "type": "number", + "description": "clockwise rotation in degrees. Mirrors shape/connector/group rotation — stored on the same a:xfrm rot attribute (degrees * 60000). Alias: rotate.", + "add": true, + "set": true, + "get": true, + "aliases": ["rotate"], + "examples": [ + "--prop rotation=45", + "--prop rotation=-90" + ], + "readback": "decimal degrees (e.g. 45 or 22.5)", + "enforcement": "report" + }, + "link": { + "type": "string", + "description": "click-hyperlink target attached to the picture's cNvPr (so the whole image is clickable). Accepts absolute URI (https://, mailto:), slide[N] for in-deck jumps, or named actions (firstslide, lastslide, nextslide, previousslide). Pass 'none' or empty to clear.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop link=https://example.com", + "--prop link='slide[3]'", + "--prop link=nextslide" + ], + "readback": "URL, slide[N], or named action", + "enforcement": "report" + }, + "tooltip": { + "type": "string", + "description": "hover tooltip text for the click-hyperlink. Applied together with `link` in a single set/add call; standalone tooltip change without link is not supported.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop link=https://example.com --prop tooltip=\"Open homepage\"" + ], + "readback": "string", + "enforcement": "report" + }, + "brightness": { + "type": "number", + "description": "luminance offset in [-100, 100]. Stored on the blip as a:lumOff (value * 1000). Combine with `contrast` to mirror PowerPoint's Picture Format → Corrections.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop brightness=40", + "--prop brightness=-20" + ], + "readback": "decimal in [-100, 100]", + "enforcement": "report" + }, + "contrast": { + "type": "number", + "description": "luminance modulation in [-100, 100]. Stored on the blip as a:lumMod (100000 + value * 1000). Combine with `brightness`.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop contrast=-30", + "--prop contrast=20" + ], + "readback": "decimal in [-100, 100]", + "enforcement": "report" + }, + "shadow": { + "type": "string", + "description": "outer shadow as `color-blur-angle-dist-opacity` (mirrors shape `shadow`). color is hex (no `#`) or scheme; blur/dist in points; angle in degrees; opacity 0-100.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop shadow=000000-10-45-6-50", + "--prop shadow=accent1-8-90-4-60" + ], + "readback": "`color-blur-angle-dist-opacity`", + "enforcement": "report" + }, + "glow": { + "type": "string", + "description": "outer glow as `color-radius-opacity` (mirrors shape `glow`). color is hex (no `#`) or scheme; radius in points; opacity 0-100.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop glow=FF0000-12-75", + "--prop glow=accent2-8-50" + ], + "readback": "`color-radius-opacity`", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/placeholder.json b/schemas/help/pptx/placeholder.json new file mode 100644 index 000000000..69d3550ab --- /dev/null +++ b/schemas/help/pptx/placeholder.json @@ -0,0 +1,90 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "placeholder", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slide[N]/placeholder[M]", "/slide[N]/shape[M]"] + }, + "note": "Aliases: ph. Inserts a Shape with PlaceholderShape nonVisual properties — geometry comes from the slide layout. Add returns /slide[N]/shape[M] (placeholder is a shape at the OOXML layer). effective.* keys behave as documented in pptx/shape.json.", + "properties": { + "phType": { + "type": "enum", + "description": "placeholder type. Required at Add. Set is intentionally not supported: phType binds the placeholder to a slide-layout slot for style/position inheritance, so changing it after creation would produce a half-bound shape rather than a typed placeholder. To change a placeholder's role, remove it and re-add with the new phType.", + "values": ["title", "body", "subtitle", "date", "footer", "slidenum", "header", "picture", "chart", "table", "diagram", "media", "obj", "clipart"], + "aliases": ["phtype", "type"], + "add": true, "set": false, "get": true, + "examples": ["--prop phType=title"], + "readback": "placeholder type string", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "placeholder name. Defaults to '{type} Placeholder {id}'.", + "add": true, "set": true, "get": true, + "examples": ["--prop name=\"Title 1\""], + "readback": "name string", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "optional initial text content.", + "add": true, "set": true, "get": true, + "examples": ["--prop text=\"Slide title\""], + "readback": "concatenated run text", + "enforcement": "report" + }, + "phIndex": { + "type": "number", + "description": "placeholder index within the slide layout (PlaceholderShape/@idx). Disambiguates same-typed placeholders (e.g. two `body` placeholders).", + "add": false, "set": false, "get": true, + "readback": "non-negative integer; key omitted when @idx absent", + "enforcement": "report" + }, + "effective.direction": { + "type": "enum", + "values": ["rtl", "ltr"], + "description": "resolved reading direction inherited from placeholder→layout→master→presentation defaults. Suppressed when 'direction' is set directly on the placeholder.", + "add": false, "set": false, "get": true, + "readback": "rtl | ltr", + "enforcement": "report" + }, + "effective.size": { + "type": "length", + "description": "resolved font size inherited from placeholder→layout→master→presentation defaults. Suppressed when 'size' is set directly on the placeholder.", + "add": false, "set": false, "get": true, + "readback": "unit-qualified pt (e.g. \"18pt\")", + "enforcement": "report" + }, + "effective.font": { + "type": "string", + "description": "resolved font name inherited from placeholder→layout→master→presentation defaults. Suppressed when 'font' is set directly on the placeholder.", + "add": false, "set": false, "get": true, + "readback": "font name", + "enforcement": "report" + }, + "effective.color": { + "type": "color", + "description": "resolved text color inherited from placeholder→layout→master→presentation defaults. Suppressed when 'color' is set directly on the placeholder.", + "add": false, "set": false, "get": true, + "readback": "#-prefixed uppercase hex (scheme colors pass through)", + "enforcement": "report" + }, + "effective.bold": { + "type": "bool", + "description": "resolved bold inherited from placeholder→layout→master→presentation defaults. Suppressed when 'bold' is set directly on the placeholder.", + "add": false, "set": false, "get": true, + "readback": "true/false", + "enforcement": "report" + }, + "isTitle": { "type":"bool", "add":false, "set":false, "get":true, "description":"true when the shape is the title placeholder (phType=title or ctrTitle).", "readback":"true on title placeholders", "enforcement":"report" }, + "inheritedFrom": { "type":"string", "add":false, "set":false, "get":true, "description":"placeholder inheritance source — `layout` when the placeholder definition lives on the parent slide layout (not the slide itself).", "readback":"`layout` when inherited", "enforcement":"report" } + } +} diff --git a/schemas/help/pptx/presentation.json b/schemas/help/pptx/presentation.json new file mode 100644 index 000000000..5ca064c4f --- /dev/null +++ b/schemas/help/pptx/presentation.json @@ -0,0 +1,376 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "presentation", + "container": true, + "operations": { + "add": false, + "set": true, + "get": true, + "query": true, + "remove": false + }, + "paths": { + "positional": [ + "/" + ] + }, + "note": "Root container. Get returns the presentation node with slide count + theme/master/layout references as children. Not addressable via Add. Set on '/' exposes core document metadata (title/author/subject/keywords/description/category) — written to docProps/core.xml, same source as docx/xlsx. Element-level mutations go through /slide[N], /theme, etc.", + "children": [ + { + "element": "slide", + "pathSegment": "slide", + "cardinality": "0..n" + }, + { + "element": "slidemaster", + "pathSegment": "slidemaster", + "cardinality": "1..n" + }, + { + "element": "theme", + "pathSegment": "theme", + "cardinality": "1" + } + ], + "extends": "_shared/root-metadata", + "properties": { + "title": { + "type": "string", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop title=\"Q4 Review\"" + ], + "readback": "title string", + "enforcement": "report" + }, + "author": { + "type": "string", + "aliases": [ + "creator" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop author=\"Alice\"" + ], + "readback": "author string", + "enforcement": "report" + }, + "keywords": { + "type": "string", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop keywords=\"tag1,tag2\"" + ], + "readback": "keywords string", + "enforcement": "report" + }, + "description": { + "type": "string", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop description=\"Abstract\"" + ], + "readback": "description string", + "enforcement": "report" + }, + "category": { + "type": "string", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop category=Marketing" + ], + "readback": "category string", + "enforcement": "report" + }, + "lastModifiedBy": { + "type": "string", + "aliases": [ + "lastmodifiedby" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop lastModifiedBy=\"Bob\"" + ], + "readback": "last-modified author", + "enforcement": "report" + }, + "revisionNumber": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "docProps/core.xml Revision field — presentation save counter.", + "examples": [ + "--prop revisionNumber=3" + ], + "readback": "revision number string", + "enforcement": "report" + }, + "created": { + "type": "string", + "description": "creation timestamp from docProps/core.xml.", + "add": false, + "set": false, + "get": true, + "readback": "ISO 8601 timestamp", + "enforcement": "report" + }, + "modified": { + "type": "string", + "description": "last-modified timestamp from docProps/core.xml.", + "add": false, + "set": false, + "get": true, + "readback": "ISO 8601 timestamp", + "enforcement": "report" + }, + "slideWidth": { + "type": "string", + "aliases": ["width"], + "description": "slide width from , formatted via FormatEmu. Set accepts unit-qualified or raw EMU; setting width or height alone switches @type to 'custom'.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop slideWidth=30cm", + "--prop slideWidth=720pt" + ], + "readback": "unit-qualified length string (e.g. '25.4cm', '720pt')", + "enforcement": "report" + }, + "slideHeight": { + "type": "string", + "aliases": ["height"], + "description": "slide height from , formatted via FormatEmu. Set accepts unit-qualified or raw EMU; setting width or height alone switches @type to 'custom'.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop slideHeight=20cm", + "--prop slideHeight=540pt" + ], + "readback": "unit-qualified length string (e.g. '19.05cm', '540pt')", + "enforcement": "report" + }, + "slideSize": { + "type": "string", + "description": "slide size preset name derived from . Set accepts any preset name and rewrites @cx/@cy + @type accordingly.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop slideSize=widescreen", + "--prop slideSize=a4" + ], + "readback": "preset name: widescreen | standard | 16:10 | a4 | a3 | letter | b4 | b5 | 35mm | overhead | banner | ledger | custom", + "enforcement": "report" + }, + "defaultFont": { + "type": "string", + "description": "default minor (body) font from the first slide master's theme FontScheme.", + "add": false, + "set": false, + "get": true, + "readback": "default text font family name", + "enforcement": "report" + }, + "extended.application": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "from docProps/app.xml application identifier (e.g. \"Microsoft PowerPoint\").", + "readback": "application string", + "enforcement": "report" + }, + "compatMode": { + "type": "bool", + "aliases": ["compatibilitymode"], + "add": false, + "set": true, + "get": true, + "description": "presentation @compatMode flag — true when the file is in legacy-compatibility mode.", + "examples": [ + "--prop compatMode=true" + ], + "readback": "true when compat mode is on", + "enforcement": "report" + }, + "firstSlideNum": { + "type": "number", + "aliases": ["firstslidenumber"], + "add": false, + "set": true, + "get": true, + "description": "presentation @firstSlideNum — slide number of the first slide (default 1).", + "examples": [ + "--prop firstSlideNum=10" + ], + "readback": "integer", + "enforcement": "report" + }, + "rtl": { + "type": "bool", + "add": false, + "set": true, + "get": false, + "description": "Set-only input alias for presentation @rtl (right-to-left reading order). Get exposes the same OOXML attribute under the canonical key `direction` ('rtl' when true), matching docx convention. Asymmetric by design — set with `rtl=true|false`, read via `direction`.", + "examples": [ + "--prop rtl=true" + ], + "enforcement": "report" + }, + "direction": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "Get-only canonical key for presentation reading direction. Emits 'rtl' when @rtl=true is set; absent otherwise. To set, use the input-only `rtl=true|false` key.", + "readback": "'rtl' when right-to-left; key absent for default left-to-right", + "enforcement": "report" + }, + "print.colorMode": { + "type": "string", + "aliases": ["printcolormode"], + "add": false, + "set": true, + "get": true, + "description": "PrintingProperties color mode. Set accepts: color|clr, grayscale|gray, blackAndWhite|bw.", + "examples": [ + "--prop print.colorMode=gray", + "--prop print.colorMode=color" + ], + "readback": "color mode token (clr | gray | bw)", + "enforcement": "report" + }, + "print.frameSlides": { + "type": "bool", + "add": false, + "set": true, + "get": true, + "description": "PrintingProperties FrameSlides flag — print a thin border around each slide.", + "examples": [ + "--prop print.frameSlides=true" + ], + "readback": "true when set", + "enforcement": "report" + }, + "print.hiddenSlides": { + "type": "bool", + "add": false, + "set": true, + "get": true, + "description": "PrintingProperties HiddenSlides flag — include hidden slides in printed output.", + "examples": [ + "--prop print.hiddenSlides=true" + ], + "readback": "true when set", + "enforcement": "report" + }, + "print.what": { + "type": "string", + "aliases": ["printwhat"], + "add": false, + "set": true, + "get": true, + "description": "PrintingProperties PrintWhat — what gets printed. Set accepts shorthand (slides, handouts, notes, outline) and explicit OOXML tokens (handouts1, handouts2, handouts3, handouts4, handouts6, handouts9). Bare `handouts` is an alias for handouts1. Get returns the OOXML token (e.g. handouts1).", + "examples": [ + "--prop print.what=handouts", + "--prop print.what=handouts4", + "--prop print.what=notes" + ], + "readback": "print-what enum inner text (slides | handouts1..9 | notes | outline)", + "enforcement": "report" + }, + "print.scaleToFitPaper": { + "type": "bool", + "add": false, + "set": true, + "get": true, + "description": "PrintingProperties ScaleToFitPaper flag — scale slides to fill the paper page.", + "examples": [ + "--prop print.scaleToFitPaper=true" + ], + "readback": "true when set", + "enforcement": "report" + }, + "show.loop": { + "type": "bool", + "aliases": ["showloop"], + "add": false, + "set": true, + "get": true, + "description": "ShowProperties Loop flag — auto-restart slideshow when reaching the end.", + "examples": [ + "--prop show.loop=true" + ], + "readback": "true when set", + "enforcement": "report" + }, + "show.narration": { + "type": "bool", + "aliases": ["shownarration"], + "add": false, + "set": true, + "get": true, + "description": "ShowProperties ShowNarration flag — play recorded narration during slideshow.", + "examples": [ + "--prop show.narration=true" + ], + "readback": "true|false", + "enforcement": "report" + }, + "show.animation": { + "type": "bool", + "aliases": ["showanimation"], + "add": false, + "set": true, + "get": true, + "description": "ShowProperties ShowAnimation flag — play animations during slideshow.", + "examples": [ + "--prop show.animation=true" + ], + "readback": "true|false", + "enforcement": "report" + }, + "show.useTimings": { + "type": "bool", + "aliases": ["usetimings", "show.usetimings"], + "add": false, + "set": true, + "get": true, + "description": "ShowProperties UseTimings flag — use stored slide timings during slideshow.", + "examples": [ + "--prop show.useTimings=true" + ], + "readback": "true|false", + "enforcement": "report" + }, + "removePersonalInfo": { + "type": "bool", + "aliases": ["removepersonalinfoonsave"], + "add": false, + "set": true, + "get": true, + "description": "ExtendedProperties RemovePersonalInfoOnSave — strip author/last-saved-by metadata on save.", + "examples": [ + "--prop removePersonalInfo=true" + ], + "readback": "true when set", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/raw.json b/schemas/help/pptx/raw.json new file mode 100644 index 000000000..7b26cf277 --- /dev/null +++ b/schemas/help/pptx/raw.json @@ -0,0 +1,29 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "raw", + "operations": { + "add": false, + "set": false, + "get": false, + "query": false, + "remove": false + }, + "properties": {}, + "description": "Raw OOXML access via the `raw` (read) and `raw-set` (write) commands. Use only when L2 DOM operations cannot express what you need. Two part-path forms are accepted: (1) semantic short names (recommended — /slide[N] is stable across reorder) and (2) zip-internal URIs (any path ending in .xml is resolved literally against the package, e.g. /ppt/slides/slide1.xml).", + "parts": [ + { "name": "/presentation", "desc": "Presentation part (slide list, sizing, defaults)" }, + { "name": "/theme", "desc": "Theme (color scheme, font scheme)" }, + { "name": "/slide[N]", "desc": "Nth slide (1-based, in visible order)" }, + { "name": "/slideMaster[N]", "desc": "Nth slide master" }, + { "name": "/slideLayout[N]", "desc": "Nth slide layout" }, + { "name": "/noteSlide[N]", "desc": "Notes slide attached to slide N" }, + { "name": "/.xml", "desc": "Any path ending in .xml is resolved as a literal zip-internal URI (e.g. /ppt/slides/slide1.xml, /ppt/slideLayouts/slideLayout3.xml, /ppt/theme/theme1.xml). Use semantic names when slides may be reordered." } + ], + "examples": [ + "officecli raw deck.pptx /presentation", + "officecli raw deck.pptx '/slide[1]'", + "officecli raw deck.pptx /ppt/slideMasters/slideMaster1.xml", + "officecli raw-set deck.pptx '/slide[1]' --xpath \"//p:sp[1]\" --action remove" + ] +} diff --git a/schemas/help/pptx/run.json b/schemas/help/pptx/run.json new file mode 100644 index 000000000..e8b275a4d --- /dev/null +++ b/schemas/help/pptx/run.json @@ -0,0 +1,143 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "run", + "elementAliases": ["r"], + "parent": "paragraph", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/shape[M]/p[K]/r[L]" + ] + }, + "note": "Appends a:r inside a:p. Font properties live on a:rPr. Colors get #-prefixed on readback via ParseHelpers.FormatHexColor. effective.* keys behave as documented in pptx/shape.json (no effective.direction on runs).", + "extends": [ + "_shared/run", + "_shared/run.docx-pptx" + ], + "properties": { + "baseline": { + "type": "string", + "description": "vertical alignment in % of font height. Accepts 'super' (≡ +30), 'sub' (≡ -25), 'none'/'false'/'0', or a signed number. Get readback is the numeric percentage.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop baseline=super", + "--prop baseline=sub", + "--prop baseline=-25" + ], + "readback": "signed number (e.g. 30, -25, 0)", + "enforcement": "strict" + }, + "kern": { + "type": "int", + "description": "minimum font size (in hundredths of a point) at which character kerning is applied. ECMA-376 §21.1.2.3.9 a:rPr/@kern. 0 disables; 1 enables for all sizes; e.g. 1200 = kern from 12pt up.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop kern=0", + "--prop kern=1200" + ], + "readback": "raw OOXML integer (hundredths of a point)", + "enforcement": "strict" + }, + "spacing": { + "type": "number", + "description": "character spacing in points. Stored as 1/100 pt in OOXML (a:rPr/@spc); readback is the point value. Negative values tighten.", + "aliases": [ + "charspacing", + "letterspacing", + "spc" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop spacing=2", + "--prop charspacing=-1", + "--prop spacing=0.5" + ], + "readback": "point value as decimal string (e.g. '2', '-1', '0.5')", + "enforcement": "strict" + }, + "lang": { + "type": "string", + "description": "BCP-47 language tag for the run (ECMA-376 §21.1.2.3.9 a:rPr/@lang). DrawingML exposes a single primary-language slot — there is no per-script (latin/ea/cs) split like Word; 'lang.latin' is accepted as an alias on input, 'lang.ea'/'lang.cs' are rejected. Validated against a BCP-47 shape, max 35 chars.", + "aliases": [ + "lang.latin" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lang=en-US", + "--prop lang=zh-CN", + "--prop lang=ja-JP" + ], + "readback": "BCP-47 tag as written (e.g. 'en-US')", + "enforcement": "strict" + }, + "cap": { + "type": "enum", + "values": [ + "none", + "small", + "all" + ], + "aliases": [ + "allCaps", + "allcaps", + "smallCaps", + "smallcaps" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop cap=all", + "--prop allCaps=true" + ], + "readback": "none | small | all", + "enforcement": "report" + }, + "effective.font": { + "type": "string", + "description": "resolved font name inherited from placeholder→layout→master→presentation defaults. Suppressed when 'font' is set directly on the run.", + "add": false, + "set": false, + "get": true, + "readback": "font name", + "enforcement": "report" + }, + "subscript": { + "type": "bool", + "description": "vertical alignment = subscript (sugar for baseline=sub). Mutually exclusive with superscript. Get readback uses canonical 'baseline' (not surfaced as 'subscript').", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop subscript=true" + ], + "enforcement": "strict" + }, + "superscript": { + "type": "bool", + "description": "vertical alignment = superscript (sugar for baseline=super). Mutually exclusive with subscript. Get readback uses canonical 'baseline' (not surfaced as 'superscript').", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop superscript=true" + ], + "enforcement": "strict" + } + } +} diff --git a/schemas/help/pptx/shape.json b/schemas/help/pptx/shape.json new file mode 100644 index 000000000..1ae0c7109 --- /dev/null +++ b/schemas/help/pptx/shape.json @@ -0,0 +1,872 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "shape", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": [ + "/slide[N]/shape[@id=ID]" + ], + "positional": [ + "/slide[N]/shape[N]" + ] + }, + "note": "Positional /shape[N] enumerates ALL shapes on the slide including layout-inherited placeholders — new shapes typically land at the end, not at /shape[1]. `add` echoes the canonical /shape[@id=ID] path; prefer that for follow-up Set/Get. effective.* keys (direction, size, font, color, bold) are read-only inheritance-resolved values walked up the placeholder→layout→master→defaults chain; suppressed when the corresponding direct key is set. Each pairs with effective.*.src naming the writing layer.", + "extends": "_shared/shape", + "properties": { + "size": { + "type": "font-size", + "description": "font size — Add/Set forwards to the first run; Get exposes the inheritance-resolved value as effective.size, not bare size.", + "aliases": ["fontSize", "fontsize", "font.size"], + "add": true, "set": true, "get": false, + "examples": ["--prop size=14", "--prop size=14pt", "--prop size=10.5pt"], + "readback": "n/a at shape level — read effective.size on the shape, or size on the inner run", + "enforcement": "strict" + }, + "evaluated": { + "type": "bool", + "description": "cross-handler protocol flag — emitted on shapes whose body contains an dynamic field (slide number, date-time, footer, etc.). True when every dynamic field has cached text the renderer can show; false when at least one is empty (view text substitutes #OCLI_NOTEVAL!{type}; view issues emits subtype slide_field_not_evaluated). PowerPoint always re-renders fields on open, so there is no cache_stale equivalent. Read this via get --json instead of pattern-matching the sentinel.", + "add": false, "set": false, "get": true, + "readback": "true|false (true ⇒ all shapes on this node carry display text)", + "enforcement": "report" + }, + "isTitle": { + "type": "bool", + "description": "true when this shape is the slide title placeholder (or a content placeholder configured as title). Always emitted on every shape so query selectors like `shape[isTitle=true]` and `shape[isTitle=false]` resolve uniformly. Read-only — title-ness is determined by placeholder type at Add time, not as a settable property.", + "add": false, "set": false, "get": true, + "readback": "true|false (always present)", + "enforcement": "report" + }, + "opacity": { + "type": "number", + "description": "fill opacity (0.0 - 1.0). Requires a fill to attach to — opacity alone (without fill/gradient/pattern) has no effect in OOXML.", + "requires": [ + "fill" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop opacity=0.5 --prop fill=FF0000" + ], + "readback": "number in [0, 1]", + "enforcement": "strict" + }, + "geometry": { + "type": "string", + "description": "Preset shape geometry (default: rect).", + "aliases": [ + "preset", + "shape" + ], + "values": [ + "rect", + "roundRect", + "ellipse", + "triangle", + "diamond", + "parallelogram", + "rightArrow", + "star5" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop geometry=ellipse", + "--prop preset=roundRect" + ], + "readback": "preset name (e.g. \"ellipse\", \"roundRect\")", + "enforcement": "strict" + }, + "font.latin": { + "type": "string", + "description": "Latin-script font slot only (a:latin). Use to target ASCII/European text without overwriting CJK / complex-script slots.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.latin=Calibri" + ], + "readback": "typeface (only emitted when it differs from the bare 'font' slot)", + "enforcement": "report" + }, + "font.ea": { + "type": "string", + "description": "East-Asian font slot (a:ea) — Chinese / Japanese / Korean text.", + "aliases": [ + "font.eastasia", + "font.eastasian" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.ea=\"メイリオ\"" + ], + "readback": "typeface (only emitted when it differs from the bare 'font' slot)", + "enforcement": "report" + }, + "font.cs": { + "type": "string", + "description": "Complex-script font slot (a:cs) — Arabic / Hebrew / Thai etc.", + "aliases": [ + "font.complexscript", + "font.complex" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop font.cs=\"Arabic Typesetting\"" + ], + "readback": "typeface", + "enforcement": "report" + }, + "direction": { + "type": "enum", + "values": [ + "ltr", + "rtl" + ], + "description": "paragraph reading direction (a:pPr rtl). Use 'rtl' for Arabic / Hebrew layouts.", + "aliases": [ + "dir", + "rtl" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop direction=rtl" + ], + "readback": "rtl (ltr is the default and is suppressed; clearing direction removes the attribute)", + "enforcement": "report" + }, + "strike": { + "type": "enum", + "description": "strikethrough on shape text. Single and double are distinct OOXML tokens (sngStrike / dblStrike); Word splits these into `strike` + `dstrike`, PowerPoint folds both into one attribute.", + "values": [ + "none", + "single", + "double" + ], + "aliases": [ + "strikethrough", + "font.strike", + "font.strikethrough" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop strike=single", + "--prop strike=double", + "--prop strike=true" + ], + "readback": "single | double | none. Boolean `true` on add/set is accepted as an alias for `single`.", + "enforcement": "report" + }, + "cap": { + "type": "enum", + "description": "letter-case rendering mode for shape text (rPr/cap).", + "values": [ + "none", + "small", + "all" + ], + "aliases": [ + "allCaps", + "allcaps", + "smallCaps", + "smallcaps" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop cap=all", + "--prop allCaps=true" + ], + "readback": "none | small | all", + "enforcement": "report" + }, + "lang": { + "type": "string", + "description": "BCP-47 language tag on first run rPr (drawingML rPr/@lang).", + "aliases": [ + "altLang", + "altlang" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lang=en-US" + ], + "readback": "BCP-47 tag", + "enforcement": "report" + }, + "spacing": { + "type": "number", + "description": "character spacing in 1/100 pt (drawingML rPr/@spc).", + "aliases": [ + "spc", + "charspacing", + "letterspacing" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop spacing=200" + ], + "readback": "integer", + "enforcement": "report" + }, + "kern": { + "type": "number", + "description": "minimum kerning size in 1/100 pt (drawingML rPr/@kern). Get surfaces the raw integer via long-tail rPr attribute readback.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop kern=1200" + ], + "readback": "integer", + "enforcement": "report" + }, + "autoFit": { + "type": "enum", + "values": [ + "normal", + "shape", + "none" + ], + "aliases": [ + "autofit" + ], + "add": true, + "set": true, + "get": true, + "description": "text body auto-fit mode. 'normal' shrinks text to fit; 'shape' resizes the shape to fit text; 'none' overflows. Aliases on Set: true/shrink → normal, resize → shape, false → none.", + "examples": [ + "--prop autoFit=normal", + "--prop autoFit=shape", + "--prop autoFit=none" + ], + "readback": "normal | shape | none", + "enforcement": "report" + }, + "lineSpacing": { + "type": "string", + "description": "line spacing for shape paragraphs (multiplier or pt).", + "aliases": [ + "linespacing" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lineSpacing=1.5x" + ], + "readback": "1.5x or 18pt", + "enforcement": "report" + }, + "spaceBefore": { + "type": "length", + "aliases": [ + "spacebefore" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop spaceBefore=6pt" + ], + "readback": "unit-qualified pt", + "enforcement": "report" + }, + "spaceAfter": { + "type": "length", + "aliases": [ + "spaceafter" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop spaceAfter=6pt" + ], + "readback": "unit-qualified pt", + "enforcement": "report" + }, + "gradient": { + "type": "string", + "description": "gradient fill spec. Linear: 'C1-C2[-ANGLE]' or 'LINEAR;C1;C2;ANGLE'. Radial: 'radial:C1-C2[-FOCUS]' (focus: tl/tr/bl/br/center). Path: 'path:C1-C2[-FOCUS]'. Per-stop position: 'C@PCT' (e.g. 'FF0000@0-0000FF@100').", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop gradient=\"FF0000-0000FF\"", + "--prop gradient=\"FF0000-0000FF-90\"", + "--prop gradient=\"LINEAR;FF0000;0000FF;45\"", + "--prop gradient=\"radial:4B0082-1E90FF-center\"" + ], + "readback": "linear: 'linear;C1;C2;ANGLE' (semicolon-separated, degree integer). radial/path: 'radial:C1-C2-FOCUS' / 'path:C1-C2-FOCUS'. When gradient is present, 'fill' reads back as 'gradient' unless a solidFill also exists.", + "enforcement": "report" + }, + "pattern": { + "type": "string", + "description": "pattern fill: 'preset' or 'preset:fg' or 'preset:fg:bg' (defaults: fg=000000, bg=FFFFFF).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop pattern=\"diagBrick:FF0000:FFFFFF\"" + ], + "readback": "preset:fg_color[:bg_color] e.g. diagBrick:#FF0000:#FFFFFF", + "enforcement": "report" + }, + "image": { + "type": "string", + "description": "image (blip) fill: path to a local image file used as the shape fill.", + "aliases": [ + "imagefill" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop image=/path/to/photo.png" + ], + "readback": "\"true\" when shape has an image (blipFill) fill", + "enforcement": "report" + }, + "lineWidth": { + "type": "length", + "description": "outline width.", + "aliases": [ + "linewidth" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lineWidth=1pt" + ], + "readback": "length in pt (e.g. \"1pt\")", + "enforcement": "report" + }, + "lineDash": { + "type": "enum", + "description": "outline dash pattern (drawingML prstDash). Also reachable as the third segment of the compound line='color:width:style' form.", + "aliases": [ + "linedash", + "line.dash" + ], + "values": [ + "solid", + "dot", + "dash", + "dashDot", + "lgDash", + "lgDashDot", + "lgDashDotDot", + "sysDot", + "sysDash", + "sysDashDot", + "sysDashDotDot", + "longdash", + "longdashdot" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lineDash=dash", + "--prop lineDash=dashDot", + "--prop lineDash=sysDash", + "--prop line=FF0000:1.5:dash" + ], + "readback": "canonical OOXML prstDash token (solid | dot | dash | dashDot | lgDash | lgDashDot | lgDashDotDot | sysDot | sysDash | sysDashDot | sysDashDotDot). 'longdash'/'longdashdot' are accepted on input as aliases for lgDash/lgDashDot. 'dashDotDot' has no native enum member; it is accepted as an alias for sysDashDotDot (Get readback returns sysDashDotDot).", + "enforcement": "report" + }, + "lineCap": { + "type": "enum", + "description": "line end cap style (drawingML ). Affects how the stroke terminates at endpoints (and dash gaps for dashed strokes).", + "aliases": [ + "linecap", + "line.cap" + ], + "values": [ + "round", + "flat", + "square" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lineCap=round" + ], + "readback": "round | flat | square", + "enforcement": "report" + }, + "lineJoin": { + "type": "enum", + "description": "line join style at shape corners (drawingML child | | ).", + "aliases": [ + "linejoin", + "line.join" + ], + "values": [ + "round", + "bevel", + "miter" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lineJoin=miter" + ], + "readback": "round | bevel | miter", + "enforcement": "report" + }, + "cmpd": { + "type": "enum", + "description": "compound line style (drawingML ) for double/triple/asymmetric strokes.", + "aliases": [ + "compoundLine", + "compoundline", + "line.compound" + ], + "values": [ + "sng", + "dbl", + "thickThin", + "thinThick", + "tri" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop cmpd=dbl", + "--prop cmpd=thickThin" + ], + "readback": "canonical OOXML token (sng | dbl | thickThin | thinThick | tri)", + "enforcement": "report" + }, + "lineAlign": { + "type": "enum", + "description": "stroke alignment relative to the path (drawingML ). 'ctr' centers the stroke on the path; 'in' insets it inside the shape boundary.", + "aliases": [ + "linealign", + "line.align" + ], + "values": [ + "ctr", + "in" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop lineAlign=ctr" + ], + "readback": "ctr | in", + "enforcement": "report" + }, + "headEnd": { + "type": "enum", + "description": "arrowhead at the line's start (drawingML ). Supported on any outline (shape or connector), not connector-only.", + "aliases": [ + "headend", + "arrowStart", + "arrowstart" + ], + "values": [ + "none", + "triangle", + "stealth", + "diamond", + "oval", + "arrow" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop headEnd=triangle" + ], + "readback": "canonical OOXML token (none | triangle | stealth | diamond | oval | arrow)", + "enforcement": "report" + }, + "tailEnd": { + "type": "enum", + "description": "arrowhead at the line's end (drawingML ). Supported on any outline (shape or connector).", + "aliases": [ + "tailend", + "arrowEnd", + "arrowend" + ], + "values": [ + "none", + "triangle", + "stealth", + "diamond", + "oval", + "arrow" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop tailEnd=arrow" + ], + "readback": "canonical OOXML token (none | triangle | stealth | diamond | oval | arrow)", + "enforcement": "report" + }, + "list": { + "type": "string", + "description": "list style for shape paragraphs (bullet|numbered|alpha|roman|none|).", + "aliases": [ + "liststyle" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop list=bullet" + ], + "readback": "n/a", + "enforcement": "report" + }, + "link": { + "type": "string", + "description": "hyperlink URL or anchor for shape click action.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop link=https://example.com" + ], + "readback": "n/a", + "enforcement": "report" + }, + "tooltip": { + "type": "string", + "description": "tooltip / screen-tip text for hyperlink. Applied together with `link` in a single set/add call; standalone tooltip without link is not supported (no hyperlink relationship to attach to). Set-side may update an existing hyperlink's tooltip in-place without re-supplying link.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop link=https://example.com --prop tooltip=\"click here\"" + ], + "readback": "n/a", + "enforcement": "report" + }, + "animation": { + "type": "string", + "description": "animation effect spec.", + "aliases": [ + "animate" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop animation=fade" + ], + "readback": "n/a", + "enforcement": "report" + }, + "effective.direction": { + "type": "enum", + "values": [ + "rtl", + "ltr" + ], + "description": "resolved reading direction inherited from placeholder→layout→master→presentation defaults. Suppressed when 'direction' is set directly on the shape.", + "add": false, + "set": false, + "get": true, + "readback": "rtl | ltr", + "enforcement": "report" + }, + "effective.size": { + "type": "length", + "description": "resolved font size inherited from placeholder→layout→master→presentation defaults. Suppressed when 'size' is set directly on the shape.", + "add": false, + "set": false, + "get": true, + "readback": "unit-qualified pt (e.g. \"18pt\")", + "enforcement": "report" + }, + "effective.font": { + "type": "string", + "description": "resolved font name inherited from placeholder→layout→master→presentation defaults. Suppressed when 'font' is set directly on the shape.", + "add": false, + "set": false, + "get": true, + "readback": "font name", + "enforcement": "report" + }, + "effective.color": { + "type": "color", + "description": "resolved text color inherited from placeholder→layout→master→presentation defaults. Suppressed when 'color' is set directly on the shape.", + "add": false, + "set": false, + "get": true, + "readback": "#-prefixed uppercase hex (scheme colors pass through)", + "enforcement": "report" + }, + "effective.bold": { + "type": "bool", + "description": "resolved bold inherited from placeholder→layout→master→presentation defaults. Suppressed when 'bold' is set directly on the shape.", + "add": false, + "set": false, + "get": true, + "readback": "true/false", + "enforcement": "report" + }, + "effective.italic": { "type": "bool", "description": "resolved italic via cascade.", "add": false, "set": false, "get": true, "readback": "true/false", "enforcement": "report" }, + "effective.underline": { "type": "string", "description": "resolved underline via cascade.", "add": false, "set": false, "get": true, "readback": "single/double/none", "enforcement": "report" }, + "effective.strike": { "type": "string", "description": "resolved strike via cascade.", "add": false, "set": false, "get": true, "readback": "single/double/none", "enforcement": "report" }, + "effective.font.latin": { "type": "string", "description": "resolved Latin font via cascade.", "add": false, "set": false, "get": true, "readback": "font name", "enforcement": "report" }, + "effective.font.ea": { "type": "string", "description": "resolved East Asian font via cascade.", "add": false, "set": false, "get": true, "readback": "font name", "enforcement": "report" }, + "effective.font.cs": { "type": "string", "description": "resolved complex script font via cascade.", "add": false, "set": false, "get": true, "readback": "font name", "enforcement": "report" }, + "effective.align": { "type": "string", "description": "resolved paragraph alignment via cascade.", "add": false, "set": false, "get": true, "readback": "left/center/right/justify", "enforcement": "report" }, + "effective.lineSpacing": { "type": "string", "description": "resolved line spacing via cascade.", "add": false, "set": false, "get": true, "readback": "multiplier (e.g. 1.5x) or fixed pt", "enforcement": "report" }, + "effective.spaceBefore": { "type": "length", "description": "resolved spaceBefore via cascade.", "add": false, "set": false, "get": true, "readback": "unit-qualified pt", "enforcement": "report" }, + "effective.spaceAfter": { "type": "length", "description": "resolved spaceAfter via cascade.", "add": false, "set": false, "get": true, "readback": "unit-qualified pt", "enforcement": "report" }, + "effective.size.src": { "type": "string", "description": "path of the cascade layer that wrote effective.size.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.font.src": { "type": "string", "description": "path of the cascade layer that wrote effective.font.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.font.latin.src": { "type": "string", "description": "path of the cascade layer that wrote effective.font.latin.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.font.ea.src": { "type": "string", "description": "path of the cascade layer that wrote effective.font.ea.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.font.cs.src": { "type": "string", "description": "path of the cascade layer that wrote effective.font.cs.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.color.src": { "type": "string", "description": "path of the cascade layer that wrote effective.color.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.bold.src": { "type": "string", "description": "path of the cascade layer that wrote effective.bold.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.italic.src": { "type": "string", "description": "path of the cascade layer that wrote effective.italic.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.underline.src": { "type": "string", "description": "path of the cascade layer that wrote effective.underline.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.strike.src": { "type": "string", "description": "path of the cascade layer that wrote effective.strike.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.align.src": { "type": "string", "description": "path of the cascade layer that wrote effective.align.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.lineSpacing.src": { "type": "string", "description": "path of the cascade layer that wrote effective.lineSpacing.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.spaceBefore.src": { "type": "string", "description": "path of the cascade layer that wrote effective.spaceBefore.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "effective.spaceAfter.src": { "type": "string", "description": "path of the cascade layer that wrote effective.spaceAfter.", "add": false, "set": false, "get": true, "readback": "path string", "enforcement": "report" }, + "id": { + "type": "number", + "description": "OOXML shape id; source of the @id in the stable path /shape[@id=ID].", + "add": false, + "set": false, + "get": true, + "readback": "integer shape id", + "enforcement": "report" + }, + "zorder": { + "type": "number", + "description": "1-based z-order in slide shape tree. On add/set, repositions the shape within the shape tree (1 = back).", + "aliases": ["z-order", "order"], + "add": true, + "set": true, + "get": true, + "examples": ["--prop zorder=1"], + "readback": "1-based integer (1 = back)", + "enforcement": "report" + }, + "bevel": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "3-D top bevel descriptor (sp3d.bevelT). Form: 'preset[-width[-height]]' with width/height in points; height defaults to width. Preset values: angle, artDeco, circle, convex, coolSlant, cross, divot, hardEdge, relaxedInset, riblet, slope, softRound. Example: 'circle-6-6'.", + "examples": [ + "--prop bevel=circle", + "--prop bevel=circle-6", + "--prop bevel=angle-8-4" + ], + "readback": "'preset' alone, or 'preset-widthPt-heightPt' when non-default sizing was written. Surfaces when sp3d.bevelT is present.", + "enforcement": "report" + }, + "bevelBottom": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "3-D bottom bevel descriptor (sp3d.bevelB). Same grammar as 'bevel'.", + "examples": [ + "--prop bevelBottom=circle-4-4" + ], + "readback": "'preset' alone, or 'preset-widthPt-heightPt'.", + "enforcement": "report" + }, + "depth": { + "type": "length", + "add": true, + "set": true, + "get": true, + "description": "3-D extrusion height (sp3d @extrusionH). Accepts the canonical length input forms — bare number is points, also accepts pt/cm/in/px/emu suffix. 'none' or '0' clears.", + "examples": [ + "--prop depth=10", + "--prop depth=10pt", + "--prop depth=0.5cm", + "--prop depth=none" + ], + "readback": "bare numeric points (e.g. '10', '14.17').", + "enforcement": "report" + }, + "lighting": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "3-D scene lighting rig name (scene3d.lightRig @rig). OOXML preset rig values: threePt, balanced, soft, harsh, flood, contrasting, morning, sunrise, sunset, chilly, freezing, flat, twoPt, glow, brightRoom.", + "examples": [ + "--prop lighting=threePt", + "--prop lighting=balanced" + ], + "readback": "OOXML preset light rig token.", + "enforcement": "report" + }, + "material": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "3-D preset material (sp3d.prstMaterial). Accepted tokens (case-insensitive): clear, darkEdge (alias dkEdge), flat, matte, metal, plastic, powder, softEdge, softMetal, translucentPowder, warmMatte, wireframe (alias wire).", + "examples": [ + "--prop material=metal", + "--prop material=plastic" + ], + "readback": "OOXML preset material token.", + "enforcement": "report" + }, + "reflection": { + "type": "string", + "description": "reflection effect. Accepts 'true' / 'tight' / 'half' / 'full' to enable a reflection of varying length, or 'none' to clear.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop reflection=true", + "--prop reflection=half", + "--prop reflection=full" + ], + "readback": "tight | half | full (mapped from a:reflection @endPos: <70000=tight, <95000=half, else=full).", + "enforcement": "report" + }, + "shadow": { + "type": "string", + "description": "outer shadow. Pass a color (e.g. '000000') or 'true' (defaults to black). Routed to text-level rPr for text-only shapes.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop shadow=#808080", + "--prop shadow=none", + "--prop shadow=000000", + "--prop shadow=true" + ], + "readback": "compound '#RRGGBBAA-blurPt-angleDeg-distPt-opacityPct' (e.g. '#00000066-4-45-3-40'). Color is CSS-form 8-digit hex with leading # (alpha last); blur, distance in pt; angle in degrees; opacity in percent (0-100).", + "enforcement": "report" + }, + "glow": { + "type": "string", + "description": "glow effect. Pass a color (e.g. '4472C4') or 'true' (defaults to accent blue).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop glow=#4472C4", + "--prop glow=4472C4", + "--prop glow=true" + ], + "readback": "compound '#RRGGBBAA-radiusPt-opacityPct' (e.g. '#4472C4BF-8-75'). Color is CSS-form 8-digit hex with leading # (alpha last); radius in pt; opacity in percent.", + "enforcement": "report" + }, + "lineOpacity": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "shape outline alpha as fraction (0..1). Surfaces when the outline carries an a:alpha child.", + "readback": "fraction 0..1", + "enforcement": "report" + }, + "x": { + "type": "length", + "description": "x in EMU/length form (e.g. 2cm). pptx absolute positioning.", + "aliases": [ + "left" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop x=2", + "--prop x=2cm", + "--prop x=1in", + "--prop x=72pt" + ], + "readback": "length string (FormatEmu)", + "enforcement": "strict" + }, + "y": { + "type": "string", + "description": "y in EMU/length form (e.g. 2cm). pptx absolute positioning.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop y=3", + "--prop y=3cm" + ], + "enforcement": "report", + "aliases": [ + "top" + ], + "readback": "length string (FormatEmu)" + }, + "width": { + "type": "string", + "description": "width in EMU/length form (e.g. 2cm). pptx absolute positioning.", + "add": true, + "set": true, + "get": true, + "aliases": [ + "w" + ], + "examples": [ + "--prop width=4", + "--prop width=6cm", + "--prop width=5cm" + ], + "enforcement": "report", + "readback": "length string (FormatEmu)" + }, + "height": { + "type": "string", + "description": "height in EMU/length form (e.g. 2cm). pptx absolute positioning.", + "add": true, + "set": true, + "get": true, + "aliases": [ + "h" + ], + "examples": [ + "--prop height=3", + "--prop height=3cm" + ], + "enforcement": "report", + "readback": "length string (FormatEmu)" + } + } +} diff --git a/schemas/help/pptx/slide.json b/schemas/help/pptx/slide.json new file mode 100644 index 000000000..a3e71dd13 --- /dev/null +++ b/schemas/help/pptx/slide.json @@ -0,0 +1,114 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "slide", + "parent": "presentation", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slide[N]"] + }, + "note": "Layouts are resolved by name (ResolveSlideLayout). 'title'/'text' shorthand inserts title/content text shapes at Add. Transition `morph…` auto-prefixes shape names. Newly added slides have no placeholders — populate via title/text shorthand, `--type placeholder --prop phType=title`, or `--type shape|textbox`.", + "properties": { + "layout": { + "type": "string", + "description": "slide layout name (e.g. 'Title Slide', 'Title and Content'). Resolved against the presentation's slide masters. The `layout` property is metadata only — placeholder slots defined in the layout are NOT auto-materialized on the slide. To populate slots: pass `--prop title=…` to auto-emit a title placeholder shape (phType=title); pass `--prop text=…` to auto-emit a body placeholder shape (phType=body); or add placeholders explicitly with `--type placeholder --prop phType=`.", + "add": true, "set": true, "get": true, + "examples": ["--prop layout=\"Title Slide\""], + "readback": "layout name string", + "enforcement": "report" + }, + "title": { + "type": "string", + "description": "title text. Creates a Title shape at Add time, carrying . To modify or read the title text after creation, target the title shape directly (e.g. /slide[N]/shape[@phType=title]).", + "add": true, "set": false, "get": false, + "examples": ["--prop title=\"Introduction\""], + "readback": "not surfaced at slide level; descend into the title shape", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "content body text. Creates a Content text shape at Add time, carrying so it binds to the layout's body slot.", + "add": true, "set": false, "get": false, + "examples": ["--prop text=\"Body text\""], + "readback": "not surfaced at slide level", + "enforcement": "report" + }, + "background": { + "type": "color", + "description": "slide background. Accepts: hex color (`#RGB`, `#RRGGBB`, `#RRGGBBAA` for solid+alpha), scheme color (`accent1`..`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`), `transparent` (or `none`/`clear`), linear gradient `C1-C2[-C3][-angle]` (angle in degrees, also accepts `LINEAR;C1;C2[;angle]`), radial gradient `radial:C1-C2[-focus]` (focus: `tl`/`tr`/`bl`/`br`/`center`), path gradient `path:C1-C2[-focus]`, and image fill `image:/path/to/file` (local path; pair with `background.mode`/`background.alpha`/`background.scale`).", + "add": true, "set": true, "get": true, + "examples": ["--prop background=#FFFFFF"], + "readback": "resolved background descriptor", + "enforcement": "report" + }, + "transition": { + "type": "string", + "description": "transition name (fade, push, wipe, morph, etc.). 'morph...' triggers auto-prefixing of shape names.", + "add": true, "set": true, "get": true, + "examples": ["--prop transition=fade"], + "readback": "transition name", + "enforcement": "report" + }, + "advanceTime": { + "type": "number", + "description": "auto-advance time in milliseconds (integer). e.g. 5000 = 5 seconds.", + "aliases": ["advancetime"], + "add": true, "set": true, "get": true, + "examples": ["--prop advanceTime=5000"], + "readback": "milliseconds (integer)", + "enforcement": "report" + }, + "advanceClick": { + "type": "bool", + "description": "advance on click.", + "aliases": ["advanceclick"], + "add": true, "set": true, "get": true, + "examples": ["--prop advanceClick=true"], + "readback": "true/false", + "enforcement": "report" + }, + "notes": { + "type": "string", + "description": "slide notes text. Set-only at creation time.", + "add": false, "set": true, "get": true, + "examples": ["--prop notes=\"Speaker notes here\""], + "readback": "notes text when present", + "enforcement": "report" + }, + "hidden": { + "type": "bool", + "description": "true when the slide is hidden from slideshow (Slide/@show=false). Surfaces only on Get/Query.", + "add": false, "set": false, "get": true, + "readback": "true if slide is hidden in slideshow", + "enforcement": "report" + }, + "layoutType": { + "type": "string", + "description": "slide layout type name as encoded on the underlying SlideLayout (e.g. blank, title, titleOnly, twoContent, obj, txAndObj). Distinct from `layout`, which is the user-facing layout display name.", + "add": false, "set": false, "get": true, + "readback": "layout type token from SlideLayout/@type", + "enforcement": "report" + }, + "background.alpha": { "type":"number", "add":false, "set":true, "get":true, "description":"slide background fill alpha as percent (0..100). Settable on existing solid or image backgrounds: on solid, rewrites the color with the given alpha (equivalent to `background=RRGGBBAA`); on image, emits ``. Surfaces on get when the resolved fill carries an alpha channel.", "readback":"integer percent 0..100", "enforcement":"report" }, + "background.crop": { "type":"string", "add":false, "set":false, "get":true, "description":"slide background image crop quad in `l,t,r,b` percent units (CT_RelativeRect).", "readback":"comma-separated `l,t,r,b` quad", "enforcement":"report" }, + "background.mode": { "type":"string", "add":false, "set":true, "get":true, "description":"slide background image fill mode — `stretch` (default), `tile`, or `center`. Settable on an existing image background; emitted on get only when non-default.", "readback":"`tile` | `center`", "enforcement":"report" }, + "background.ref": { "type":"number", "add":false, "set":false, "get":true, "description":"theme background reference index — bgRef/@idx (1001/1002/1003 etc.) when the slide inherits from the theme.", "readback":"integer theme bg ref id", "enforcement":"report" }, + "background.scale": { "type":"number", "add":false, "set":true, "get":true, "description":"tile-fill scale percent (sx, both axes), 1..500. Settable on an existing image background with background.mode=tile; surfaces on get with background.mode=tile.", "readback":"integer percent", "enforcement":"report" }, + "matchedShapes": { "type":"number", "add":false, "set":false, "get":true, "description":"morph transition: number of shapes from the previous slide that matched on the current slide.", "readback":"integer match count", "enforcement":"report" }, + "morphMode": { "type":"string", "add":false, "set":false, "get":true, "description":"morph transition mode (byObject default, or other p:morph @option token).", "readback":"morph mode token", "enforcement":"report" }, + "morphShapes": { "type":"number", "add":false, "set":false, "get":true, "description":"morph transition: number of candidate shapes considered for matching on this slide.", "readback":"integer candidate count", "enforcement":"report" } + }, + "children": [ + { "element": "shape", "pathSegment": "shape", "cardinality": "0..n" }, + { "element": "table", "pathSegment": "table", "cardinality": "0..n" }, + { "element": "chart", "pathSegment": "chart", "cardinality": "0..n" }, + { "element": "picture", "pathSegment": "picture", "cardinality": "0..n" }, + { "element": "placeholder", "pathSegment": "placeholder", "cardinality": "0..n" } + ] +} diff --git a/schemas/help/pptx/slidelayout.json b/schemas/help/pptx/slidelayout.json new file mode 100644 index 000000000..e73557137 --- /dev/null +++ b/schemas/help/pptx/slidelayout.json @@ -0,0 +1,63 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "slidelayout", + "parent": "slidemaster", + "container": true, + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slidemaster[N]/slidelayout[M]", + "/slidelayout[M]", + "/slidemaster[N]/slidelayout[M]/shape[K]", + "/slidelayout[M]/shape[K]" + ] + }, + "note": "Slide layout definition. Referenced by slides via the 'layout' property. The layout part itself is read-only (created by templates), but its shape tree accepts add/set/remove via /slidelayout[N]/shape[K] (or the nested /slidemaster[N]/slidelayout[M]/shape[K] form) — same property surface as slide shapes. A layout shape appears on every slide that uses that layout. Access via path /slidelayout[N]; 'help pptx slidelayout' is the canonical lookup — 'layout' is not accepted as an element alias.", + "properties": { + "name": { + "type": "string", + "description": "layout display name (e.g. 'Title Slide').", + "add": false, "set": true, "get": true, + "readback": "layout name string", + "enforcement": "report" + }, + "background": { + "type": "color", + "description": "layout background — propagates to every slide bound to this layout unless the slide overrides. Accepts hex color (#FF6600), bare hex (FF6600), theme color (accent1/dark1/...), gradient (`C1-C2[-angle]`), image (`image:/path`), or `none` to clear (re-inherits from the owning master).", + "add": false, "set": true, "get": true, + "examples": ["--prop background=FF6600", "--prop background=accent1", "--prop background=FF0000-0000FF-90", "--prop background=image:/path/to/bg.png", "--prop background=none"], + "readback": "resolved background descriptor (hex, scheme name, gradient form, or `image`)", + "enforcement": "report" + }, + "background.alpha": { "type": "number", "add": false, "set": true, "get": true, "description": "layout background fill alpha as percent (0..100). Settable on an existing image background; surfaces on get when the resolved fill carries an alpha channel.", "readback": "integer percent 0..100", "enforcement": "report" }, + "background.crop": { "type": "string", "add": false, "set": false, "get": true, "description": "layout background image crop quad in `l,t,r,b` percent units.", "readback": "comma-separated `l,t,r,b` quad", "enforcement": "report" }, + "background.mode": { "type": "string", "add": false, "set": true, "get": true, "description": "layout background image fill mode — `stretch` (default), `tile`, or `center`. Settable on an existing image background; emitted on get only when non-default.", "readback": "`tile` | `center`", "enforcement": "report" }, + "background.ref": { "type": "number", "add": false, "set": false, "get": true, "description": "theme background reference index — bgRef/@idx when the layout inherits from the theme.", "readback": "integer theme bg ref id", "enforcement": "report" }, + "background.scale": { "type": "number", "add": false, "set": true, "get": true, "description": "tile-fill scale percent (sx, both axes), 1..500. Settable on an existing image background with background.mode=tile; surfaces on get with background.mode=tile.", "readback": "integer percent", "enforcement": "report" }, + "direction": { + "type": "string", + "description": "default reading direction. Cascades `` onto every placeholder paragraph in this layout, and stamps the owning master's txStyles Level1 default (since blank layouts have no placeholders for inheriting shapes to probe). Accepts `ltr`/`rtl` or boolean for the `rtl` alias.", + "add": false, "set": true, "get": false, + "aliases": ["dir", "rtl"], + "examples": ["--prop direction=rtl"], + "enforcement": "report" + }, + "type": { + "type": "string", + "description": "layout type (title, obj, twoObj, etc.).", + "add": false, "set": false, "get": true, + "readback": "SlideLayoutValues.InnerText", + "enforcement": "report" + } + }, + "children": [ + { "element": "shape", "pathSegment": "shape", "cardinality": "0..n" } + ] +} diff --git a/schemas/help/pptx/slidemaster.json b/schemas/help/pptx/slidemaster.json new file mode 100644 index 000000000..6b7852a11 --- /dev/null +++ b/schemas/help/pptx/slidemaster.json @@ -0,0 +1,73 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "slidemaster", + "parent": "presentation", + "container": true, + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slidemaster[N]", "/slidemaster[N]/shape[K]"] + }, + "note": "Slide master definition. Children: slideLayout references + master-level shapes (logo, watermark, default text frames) that appear on every slide. The master part itself is read-only (created by templates), but its shape tree accepts add/set/remove via /slidemaster[N]/shape[K] — same property surface as slide shapes (text, fill, line, geometry, position, font). Access via path /slidemaster[N]; 'help pptx slidemaster' is the canonical lookup — 'master' is not accepted as an element alias.", + "properties": { + "name": { + "type": "string", + "description": "master part name from CommonSlideData/@name.", + "add": false, "set": true, "get": true, + "readback": "master name string", + "enforcement": "report" + }, + "background": { + "type": "color", + "description": "master background — propagates to every slide bound (directly or via a layout) to this master unless the slide or layout overrides. Accepts hex color (#FF6600), bare hex (FF6600), theme color (accent1/dark1/...), gradient (`C1-C2[-angle]`), image (`image:/path`), or `none` to clear.", + "add": false, "set": true, "get": true, + "examples": ["--prop background=FF6600", "--prop background=accent1", "--prop background=FF0000-0000FF-90", "--prop background=image:/path/to/bg.png", "--prop background=none"], + "readback": "resolved background descriptor (hex, scheme name, gradient form, or `image`)", + "enforcement": "report" + }, + "background.alpha": { "type": "number", "add": false, "set": true, "get": true, "description": "master background fill alpha as percent (0..100). Settable on an existing image background; surfaces on get when the resolved fill carries an alpha channel.", "readback": "integer percent 0..100", "enforcement": "report" }, + "background.crop": { "type": "string", "add": false, "set": false, "get": true, "description": "master background image crop quad in `l,t,r,b` percent units.", "readback": "comma-separated `l,t,r,b` quad", "enforcement": "report" }, + "background.mode": { "type": "string", "add": false, "set": true, "get": true, "description": "master background image fill mode — `stretch` (default), `tile`, or `center`. Settable on an existing image background; emitted on get only when non-default.", "readback": "`tile` | `center`", "enforcement": "report" }, + "background.ref": { "type": "number", "add": false, "set": false, "get": true, "description": "theme background reference index — bgRef/@idx when the master inherits from the theme.", "readback": "integer theme bg ref id", "enforcement": "report" }, + "background.scale": { "type": "number", "add": false, "set": true, "get": true, "description": "tile-fill scale percent (sx, both axes), 1..500. Settable on an existing image background with background.mode=tile; surfaces on get with background.mode=tile.", "readback": "integer percent", "enforcement": "report" }, + "direction": { + "type": "string", + "description": "default reading direction propagated into the master's txStyles (Level1ParagraphProperties of body/title/other) and cascaded onto existing placeholder paragraphs. Accepts `ltr`/`rtl` or boolean for the `rtl` alias.", + "add": false, "set": true, "get": false, + "aliases": ["dir", "rtl"], + "examples": ["--prop direction=rtl"], + "enforcement": "report" + }, + "layoutCount": { + "type": "number", + "description": "number of slide layouts associated with this master.", + "add": false, "set": false, "get": true, + "readback": "integer count of associated slide layouts", + "enforcement": "report" + }, + "theme": { + "type": "string", + "description": "name of the theme attached to this master, when the theme has a name.", + "add": false, "set": false, "get": true, + "readback": "theme name string (absent if theme has no name)", + "enforcement": "report" + }, + "shapeCount": { + "type": "number", + "description": "count of background shapes (Shape + Picture) on the master's shape tree.", + "add": false, "set": false, "get": true, + "readback": "count of background shapes on the master", + "enforcement": "report" + } + }, + "children": [ + { "element": "slidelayout", "pathSegment": "slidelayout", "cardinality": "1..n" }, + { "element": "shape", "pathSegment": "shape", "cardinality": "0..n" } + ] +} diff --git a/schemas/help/pptx/table-cell.json b/schemas/help/pptx/table-cell.json new file mode 100644 index 000000000..d9fc66879 --- /dev/null +++ b/schemas/help/pptx/table-cell.json @@ -0,0 +1,418 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "cell", + "elementAliases": ["tc"], + "parent": "row", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/table[M]/tr[R]/tc[C]" + ] + }, + "note": "Appending a cell to a row is unusual — normally cells are seeded at row-Add time. This op exists for completeness; pptx tables are strictly rectangular, so a standalone Add cell only succeeds when the row currently has fewer cells than the table grid (an OOXML-illegal state).", + "extends": "_shared/table-cell", + "properties": { + "text": { + "type": "string", + "description": "single-run text content. pptx-only override: prefer set tc[C] for cell text in pptx; standalone Add cell rarely has room to land within a rectangular table grid. Set + Get unchanged.", + "add": false, "set": true, "get": true, + "examples": ["--prop text=\"Hello\""], + "readback": "string concatenated from all / in the cell paragraphs", + "enforcement": "strict" + }, + "colspan": { + "type": "number", + "description": "horizontal cell span (a:tc @gridSpan). Surfaces only when the cell spans more than one column. Read-only — set spans by structuring the table at Add time.", + "add": false, "set": false, "get": true, + "readback": "integer ≥ 2", + "enforcement": "report" + }, + "rowspan": { + "type": "number", + "description": "vertical cell span (a:tc @rowSpan). Surfaces only when the cell spans more than one row. Read-only — set spans by structuring the table at Add time.", + "add": false, "set": false, "get": true, + "readback": "integer ≥ 2", + "enforcement": "report" + }, + "padding.left": { + "type": "string", + "description": "left cell padding (a:tcPr @marL). Read-only readback; emitted as a unit-qualified length (cm/in/pt).", + "add": false, "set": false, "get": true, + "readback": "unit-qualified length, e.g. '0.25cm'", + "enforcement": "report" + }, + "padding.right": { + "type": "string", + "description": "right cell padding (a:tcPr @marR). Read-only readback; emitted as a unit-qualified length.", + "add": false, "set": false, "get": true, + "readback": "unit-qualified length, e.g. '0.25cm'", + "enforcement": "report" + }, + "padding.top": { + "type": "string", + "description": "top cell padding (a:tcPr @marT). Read-only readback; emitted as a unit-qualified length.", + "add": false, "set": false, "get": true, + "readback": "unit-qualified length, e.g. '0.13cm'", + "enforcement": "report" + }, + "padding.bottom": { + "type": "string", + "description": "bottom cell padding (a:tcPr @marB). Read-only readback; emitted as a unit-qualified length.", + "add": false, "set": true, "get": true, + "aliases": ["margin.bottom"], + "examples": ["--prop padding.bottom=0.25cm"], + "readback": "unit-qualified length, e.g. '0.13cm'", + "enforcement": "report" + }, + "padding": { + "type": "string", + "description": "shorthand to set all four cell paddings at once (a:tcPr @marL/@marR/@marT/@marB). Accepts a unit-qualified length applied to every edge.", + "aliases": ["margin"], + "add": false, "set": true, "get": false, + "examples": ["--prop padding=0.2cm"], + "enforcement": "report" + }, + "valign": { + "type": "string", + "description": "vertical text anchor (a:tcPr @anchor). top|center|bottom.", + "add": true, "set": true, "get": true, + "examples": ["--prop valign=center"], + "readback": "top|center|bottom", + "enforcement": "report" + }, + "wrap": { + "type": "bool", + "description": "cell text wrap (a:txBody/a:bodyPr @wrap). true=square (wrap), false=none (clip to cell). Set on the cell's body properties.", + "aliases": ["wordwrap"], + "add": false, "set": true, "get": true, + "examples": ["--prop wrap=false"], + "readback": "true|false", + "enforcement": "report" + }, + "textdirection": { + "type": "string", + "description": "cell text rotation (a:tcPr @vert). horizontal|vertical90|vertical270|stacked. Round-trips through the same canonical vocabulary.", + "aliases": ["textdir", "vert"], + "add": false, "set": true, "get": true, + "examples": [ + "--prop textdirection=vertical270", + "--prop textdirection=stacked" + ], + "readback": "horizontal|vertical90|vertical270|stacked", + "enforcement": "report" + }, + "direction": { + "type": "string", + "description": "first-paragraph reading direction (a:pPr @rtl). ltr|rtl. Fans out to every paragraph in the cell's text body; readback reflects the first paragraph only.", + "aliases": ["dir", "rtl"], + "add": false, "set": true, "get": true, + "examples": ["--prop direction=rtl"], + "readback": "ltr|rtl", + "enforcement": "report" + }, + "merge.right": { + "type": "number", + "description": "convenience for horizontal merge — stamps gridSpan=N on this cell and hMerge=true on the next N-1 cells in the same row.", + "add": false, "set": true, "get": false, + "examples": ["--prop merge.right=2"], + "enforcement": "report" + }, + "merge.down": { + "type": "number", + "description": "convenience for vertical merge — stamps rowSpan=N on this cell and vMerge=true on the same column in the next N-1 rows.", + "add": false, "set": true, "get": false, + "examples": ["--prop merge.down=2"], + "enforcement": "report" + }, + "bevel": { + "type": "string", + "description": "3-D cell bevel (a:tcPr/a:cell3D). 'none' to clear, or a preset name (circle, slope, cross, angle, softRound, convex, coolSlant, divot, riblet, hardEdge, artDeco, …). Optional 'NAME;Wpt;Hpt' to set width/height.", + "aliases": ["cell3d"], + "add": false, "set": true, "get": false, + "examples": ["--prop bevel=circle", "--prop bevel=\"circle;6pt;6pt\""], + "enforcement": "report" + }, + "opacity": { + "type": "number", + "description": "fill alpha as a 0–1 fraction or 0–100 percent (post-applied to the cell's solid/gradient fill).", + "aliases": ["fill.opacity", "alpha", "fill.alpha"], + "add": false, "set": true, "get": false, + "examples": ["--prop opacity=0.5", "--prop opacity=50%"], + "enforcement": "report" + }, + "image": { + "type": "string", + "description": "embed an image file as the cell's blip fill (a:tcPr/a:blipFill). Path to a local image; relationship is added to the slide. Replaces any prior fill.", + "aliases": ["imagefill"], + "add": false, "set": true, "get": false, + "examples": ["--prop image=logo.png"], + "enforcement": "report" + }, + "linespacing": { + "type": "string", + "description": "line spacing applied to every paragraph in the cell. Accepts multiplier ('1.5x', '150%') or fixed points ('18pt').", + "add": false, "set": true, "get": true, + "examples": ["--prop linespacing=1.5x", "--prop linespacing=18pt"], + "readback": "multiplier (e.g. '1.5x') or unit-qualified length (e.g. '18pt')", + "enforcement": "report" + }, + "spacebefore": { + "type": "string", + "description": "space before every paragraph in the cell. Unit-qualified length ('12pt', '0.5cm').", + "add": false, "set": true, "get": true, + "examples": ["--prop spacebefore=6pt"], + "readback": "unit-qualified length", + "enforcement": "report" + }, + "spaceafter": { + "type": "string", + "description": "space after every paragraph in the cell. Unit-qualified length ('12pt', '0.5cm').", + "add": false, "set": true, "get": true, + "examples": ["--prop spaceafter=6pt"], + "readback": "unit-qualified length", + "enforcement": "report" + }, + "align": { + "type": "string", + "description": "first-paragraph horizontal alignment applied to every paragraph in the cell (a:pPr @algn). left|center|right|justify. Readback reflects the first paragraph only.", + "aliases": ["alignment", "halign"], + "add": false, "set": true, "get": true, + "examples": ["--prop align=center"], + "readback": "left|center|right|justify", + "enforcement": "report" + }, + "font": { + "type": "string", + "description": "Latin (and East Asian fallback) font typeface for every run in the cell. Fans out to a:rPr/a:latin + a:ea on every existing run.", + "aliases": ["font.name", "font.latin"], + "add": false, "set": true, "get": true, + "examples": ["--prop font=Calibri"], + "readback": "typeface name", + "enforcement": "report" + }, + "size": { + "type": "string", + "description": "font size for every run in the cell (a:rPr @sz). Unit-qualified ('14pt') or bare number (points).", + "aliases": ["font.size", "fontsize"], + "add": false, "set": true, "get": true, + "examples": ["--prop size=14pt", "--prop size=10.5"], + "readback": "unit-qualified length, e.g. '14pt'", + "enforcement": "report" + }, + "bold": { + "type": "bool", + "description": "bold for every run in the cell (a:rPr @b).", + "aliases": ["font.bold"], + "add": false, "set": true, "get": true, + "examples": ["--prop bold=true"], + "readback": "true|false", + "enforcement": "report" + }, + "italic": { + "type": "bool", + "description": "italic for every run in the cell (a:rPr @i).", + "aliases": ["font.italic"], + "add": false, "set": true, "get": true, + "examples": ["--prop italic=true"], + "readback": "true|false", + "enforcement": "report" + }, + "underline": { + "type": "string", + "description": "underline style for every run in the cell (a:rPr @u). single|double|none or any OOXML underline value.", + "aliases": ["font.underline"], + "add": false, "set": true, "get": true, + "examples": ["--prop underline=single", "--prop underline=double"], + "readback": "single|double|… (OOXML token)", + "enforcement": "report" + }, + "strike": { + "type": "string", + "description": "strikethrough for every run in the cell (a:rPr @strike). single|double|none.", + "aliases": ["strikethrough", "font.strike", "font.strikethrough"], + "add": false, "set": true, "get": true, + "examples": ["--prop strike=single"], + "readback": "single|double|none", + "enforcement": "report" + }, + "color": { + "type": "color", + "description": "font color for every run in the cell (a:rPr/a:solidFill).", + "aliases": ["font.color"], + "add": false, "set": true, "get": true, + "examples": ["--prop color=FF0000", "--prop color=red"], + "readback": "#RRGGBB uppercase or scheme color name", + "enforcement": "report" + }, + "fill": { + "type": "color", + "description": "cell background fill. Accepts a solid color (hex, named, rgb(...)), scheme color name (accent1–accent6, dk1, dk2, lt1, lt2, hyperlink), 'none' for explicit no-fill, or a gradient string 'COLOR1-COLOR2[-ANGLE]' (e.g. 'FF0000-0000FF-90'). Stored as a:solidFill/a:noFill/a:gradFill on a:tcPr.", + "aliases": [ + "background", + "shd", + "shading" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop fill=FFFF00", + "--prop fill=#FF0000", + "--prop fill=red", + "--prop background=accent1", + "--prop fill=none", + "--prop fill=\"FF0000-0000FF-90\"" + ], + "readback": "#RRGGBB uppercase, 'gradient' (with separate 'gradient' key), or 'image' for picture fill", + "enforcement": "report" + }, + "baseline": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "first run baseline offset for the cell text (percent units; positive=raised).", + "readback": "percent (e.g. 30)", + "enforcement": "report" + }, + "gridSpan": { + "type": "number", + "add": false, + "set": true, + "get": true, + "description": "horizontal merge span — number of grid columns this cell spans (>=2 means merged across). Setting gridSpan=N also stamps hMerge=true on the next (N-1) cells in the same row, matching the convenience prop merge.right.", + "readback": "integer span", + "examples": ["--prop gridSpan=3"], + "enforcement": "report" + }, + "hmerge": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "true on cells continued from a horizontal merge anchor (CT_TableCell @hMerge).", + "readback": "true on continuation cells", + "enforcement": "report" + }, + "vmerge": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "true on cells continued from a vertical merge anchor (CT_TableCell @vMerge). Surfaced by Query for table cells.", + "readback": "true on continuation cells", + "enforcement": "report" + }, + "image.relId": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "relationship id of an embedded image used as the cell's blip fill.", + "readback": "relationship id token", + "enforcement": "report" + }, + "border.all": { + "type": "string", + "description": "all four cell edges. Format: 'WIDTH[ DASH][ COLOR]' (e.g. '1pt solid FF0000') or 'STYLE;WIDTH;COLOR[;DASH]' (style ignored — kept for docx parity). DASH ∈ solid|dot|dash|lgDash|dashDot|sysDot|sysDash. Use 'none' to clear. Alias: border. Stored as a:lnL/lnR/lnT/lnB on a:tcPr. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "aliases": [ + "border" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop border.all=\"1pt solid FF0000\"", + "--prop border=none", + "--prop border.all=\"single;4;FF0000\"" + ], + "enforcement": "report", + "readback": "edge descriptor (e.g. 'solid;4;FF0000')" + }, + "border.bottom": { + "type": "string", + "description": "bottom border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR' (SIZE is in 1/8 pt units).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop border.bottom=\"1pt solid 808080\"", + "--prop border.bottom=\"double;6;0000FF\"" + ], + "enforcement": "report", + "readback": "edge descriptor (e.g. 'solid;4;FF0000')" + }, + "border.left": { + "type": "string", + "description": "left border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR' (SIZE is in 1/8 pt units).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop border.left=\"1pt solid 808080\"", + "--prop border.left=\"single;4\"" + ], + "enforcement": "report", + "readback": "edge descriptor (e.g. 'solid;4;FF0000')" + }, + "border.right": { + "type": "string", + "description": "right border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR' (SIZE is in 1/8 pt units).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop border.right=\"1pt solid 808080\"", + "--prop border.right=\"single;4\"" + ], + "enforcement": "report", + "readback": "edge descriptor (e.g. 'solid;4;FF0000')" + }, + "border.tl2br": { + "type": "string", + "description": "diagonal from top-left to bottom-right (a:lnTlToBr). Format same as border.all. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient. Add/Set only — Get does not surface diagonal borders today.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.tl2br=\"1pt solid FF0000\"", + "--prop border.tl2br=\"single;4;FF0000\"" + ], + "enforcement": "report", + "readback": "n/a (Get does not surface diagonal borders)" + }, + "border.top": { + "type": "string", + "description": "top border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR' (SIZE is in 1/8 pt units).", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop border.top=\"2pt solid 000000\"", + "--prop border.top=\"single;4;000000\"" + ], + "enforcement": "report", + "readback": "edge descriptor (e.g. 'solid;4;FF0000')" + }, + "border.tr2bl": { + "type": "string", + "description": "diagonal from top-right to bottom-left (a:lnBlToTr). Format same as border.all. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient. Add/Set only — Get does not surface diagonal borders today.", + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.tr2bl=\"1pt solid FF0000\"", + "--prop border.tr2bl=\"single;4;FF0000\"" + ], + "enforcement": "report", + "readback": "n/a (Get does not surface diagonal borders)" + } + } +} diff --git a/schemas/help/pptx/table-column.json b/schemas/help/pptx/table-column.json new file mode 100644 index 000000000..e4bcd9759 --- /dev/null +++ b/schemas/help/pptx/table-column.json @@ -0,0 +1,36 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "column", + "elementAliases": ["col"], + "parent": "table", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slide[N]/table[M]/col[C]"] + }, + "note": "Adds a GridColumn plus a new TableCell in every existing row at the insertion index.", + "properties": { + "width": { + "type": "length", + "description": "column width in EMU-parseable length. Defaults to average of existing columns or ~2.54cm.", + "add": true, "set": true, "get": true, + "examples": ["--prop width=3cm"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "seed text inserted into every new cell of this column.", + "add": true, "set": false, "get": false, + "examples": ["--prop text=Header"], + "readback": "not surfaced at column level", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/pptx/table-row.json b/schemas/help/pptx/table-row.json new file mode 100644 index 000000000..f56842435 --- /dev/null +++ b/schemas/help/pptx/table-row.json @@ -0,0 +1,21 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "row", + "elementAliases": ["tr"], + "parent": "table", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/slide[N]/table[M]/tr[R]" + ] + }, + "note": "Row inherits column count from the table grid unless 'cols' override is supplied. Per-cell seed text via c{N}=value props.", + "extends": "_shared/table-row" +} diff --git a/schemas/help/pptx/table.json b/schemas/help/pptx/table.json new file mode 100644 index 000000000..bed409366 --- /dev/null +++ b/schemas/help/pptx/table.json @@ -0,0 +1,315 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "table", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": [ + "/slide[N]/table[@id=ID]", + "/slide[N]/table[@name=NAME]" + ], + "positional": [ + "/slide[N]/table[M]" + ] + }, + "note": "GraphicFrame wrapping Drawing.Table. Seed data inline via `data` (semicolons split rows, commas split cells) or per-cell via `r{R}c{C}` props. `get /slide[N]/table[K]` defaults to --depth=1 (table + row stubs only; large tables at depth=2 dominate the response). Use --depth 2 or address cells directly via `/slide[N]/table[K]/row[R]/cell[C]`.", + "children": [ + { + "element": "row", + "pathSegment": "tr", + "cardinality": "1..n" + }, + { + "element": "column", + "pathSegment": "col", + "cardinality": "1..n" + } + ], + "extends": [ + "_shared/table", + "_shared/table.docx-pptx", + "_shared/table.pptx-xlsx" + ], + "properties": { + "id": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "OOXML graphic frame id (cNvPr id). pptx-only readback.", + "readback": "integer (cNvPr graphic frame id)", + "enforcement": "report" + }, + "zorder": { + "type": "number", + "description": "1-based z-order in slide shape tree. On add, positions the table within the shape tree (1 = back). Post-creation reordering is not supported via Set; use Move/Swap on the table graphic frame, or raw-set the spTree child order.", + "aliases": ["z-order", "order"], + "add": true, + "set": false, + "get": true, + "examples": ["--prop zorder=1"], + "readback": "1-based integer (1 = back)", + "enforcement": "report" + }, + "x": { + "type": "length", + "description": "left offset in EMU-parseable length.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop x=1in" + ], + "readback": "cm-formatted length", + "enforcement": "report" + }, + "y": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop y=1in" + ], + "readback": "cm-formatted length", + "enforcement": "report" + }, + "height": { + "type": "length", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop height=5cm" + ], + "readback": "cm-formatted length", + "enforcement": "report" + }, + "rowHeight": { + "type": "length", + "description": "uniform row height (EMU). If unspecified, derived from 'height' / rows.", + "aliases": [ + "rowheight" + ], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop rowHeight=1cm" + ], + "readback": "not surfaced at table level", + "enforcement": "strict" + }, + "colWidths": { + "type": "string", + "description": "per-column widths as comma- or semicolon-separated lengths (EMU/cm/in/pt). Length must match 'cols'; extra/missing entries fall back to the uniform default. Get readback is comma-separated FormatEmu output.", + "aliases": [ + "colwidths" + ], + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop colWidths=2cm,3cm,2cm", + "--prop colWidths=\"1in;2in;1in\"" + ], + "readback": "comma-separated length list (e.g. \"2cm,3cm,2cm\")", + "enforcement": "strict" + }, + "headerFill": { + "type": "color", + "description": "solid fill color applied to row 0 (header).", + "aliases": [ + "headerfill" + ], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop headerFill=#4472C4" + ], + "readback": "per-cell fill, not aggregated at table level", + "enforcement": "strict" + }, + "bodyFill": { + "type": "color", + "description": "solid fill color applied to rows 1..N (body).", + "aliases": [ + "bodyfill" + ], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop bodyFill=#EEECE1" + ], + "readback": "per-cell fill, not aggregated at table level", + "enforcement": "strict" + }, + "firstRow": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "description": "tblPr @firstRow flag — header-row styling enabled.", + "readback": "true|false", + "enforcement": "report" + }, + "lastRow": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "description": "tblPr @lastRow flag — total-row styling enabled.", + "readback": "true|false", + "enforcement": "report" + }, + "firstCol": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "description": "tblPr @firstCol flag — first-column styling enabled.", + "readback": "true|false", + "enforcement": "report" + }, + "lastCol": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "description": "tblPr @lastCol flag — last-column styling enabled.", + "readback": "true|false", + "enforcement": "report" + }, + "bandedRows": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "description": "tblPr @bandRow flag — alternating row banding from the table style.", + "readback": "true|false", + "enforcement": "report" + }, + "bandedCols": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "description": "tblPr @bandCol flag — alternating column banding from the table style.", + "readback": "true|false", + "enforcement": "report" + }, + "border.all": { + "type": "string", + "description": "shorthand: applies the border to every edge of every cell. PPT OOXML has no table-level border element — this fans out to per-cell a:lnL/lnR/lnT/lnB. Format: 'WIDTH[ DASH][ COLOR]' space-separated (e.g. '1pt solid FF0000') or 'STYLE;WIDTH;COLOR[;DASH]' semicolon form (style is ignored — kept for docx parity). DASH ∈ solid|dot|dash|lgDash|dashDot|sysDot|sysDash. Use 'none' to clear. Alias: border. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "aliases": [ + "border" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop border.all=\"1pt solid FF0000\"", + "--prop border=\"single;1pt;000000\"", + "--prop border.all=none", + "--prop border.all=\"single;4;FF0000\"" + ], + "enforcement": "report", + "readback": "edge descriptor" + }, + "border.bottom": { + "type": "string", + "description": "outer bottom border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR'. Add/Set only — read per-cell.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop border.bottom=\"2pt solid 000000\"", + "--prop border.bottom=\"double;6;0000FF\"" + ], + "enforcement": "report", + "readback": "edge descriptor" + }, + "border.horizontal": { + "type": "string", + "description": "inside-horizontal dividers (between rows). Fans out to bottom of rows 1..N-1 plus top of rows 2..N. PPT has no native inside-border element. Alias: border.insideH. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "aliases": [ + "border.insideh", + "border.insideH" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.horizontal=\"1pt solid CCCCCC\"", + "--prop border.horizontal=\"single;4;CCCCCC\"" + ], + "enforcement": "report", + "readback": "n/a (PPT has no native inside-border emit on Get)" + }, + "border.left": { + "type": "string", + "description": "outer left edge: applies to the left of column-1 cells in every row only. Format same as border.all. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop border.left=\"1pt solid 808080\"", + "--prop border.left=\"single;4\"" + ], + "enforcement": "report", + "readback": "edge descriptor" + }, + "border.right": { + "type": "string", + "description": "outer right edge: applies to the right of last-column cells in every row only. Format same as border.all. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop border.right=\"1pt solid 808080\"", + "--prop border.right=\"single;4\"" + ], + "enforcement": "report", + "readback": "edge descriptor" + }, + "border.top": { + "type": "string", + "description": "outer top border. Format: STYLE[;SIZE[;COLOR[;SPACE]]]. Cross-format note: pptx accepts a space-separated 'WIDTH DASH COLOR' form; docx only accepts the semicolon form 'STYLE;SIZE;COLOR' (SIZE is in 1/8 pt units). Add/Set only — table-level border readback is not surfaced today; inspect per-cell border.top instead.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop border.top=\"2pt solid 000000\"", + "--prop border.top=\"single;4;000000\"" + ], + "enforcement": "report", + "readback": "edge descriptor" + }, + "border.vertical": { + "type": "string", + "description": "inside-vertical dividers (between columns). Fans out to right of cols 1..M-1 plus left of cols 2..M. Alias: border.insideV. Cross-format note: docx only accepts the semicolon form 'STYLE;SIZE;COLOR' — pptx is more lenient.", + "aliases": [ + "border.insidev", + "border.insideV" + ], + "add": true, + "set": true, + "get": false, + "examples": [ + "--prop border.vertical=\"1pt solid CCCCCC\"", + "--prop border.vertical=\"single;4;CCCCCC\"" + ], + "enforcement": "report", + "readback": "n/a (PPT has no native inside-border emit on Get)" + } + } +} \ No newline at end of file diff --git a/schemas/help/pptx/textbox.json b/schemas/help/pptx/textbox.json new file mode 100644 index 000000000..9cdb4de11 --- /dev/null +++ b/schemas/help/pptx/textbox.json @@ -0,0 +1,244 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "textbox", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slide[N]/shape[M]"] + }, + "note": "Alias for shape — both route to AddShape (with --prop formula → AddEquation). Common text/position/size/font props inlined below; full surface (geometry, rotation, effects, …) in pptx/shape.json. effective.* keys behave as documented in pptx/shape.json.", + "properties": { + "text": { + "type": "string", + "add": true, "set": true, "get": true, + "examples": ["--prop text=\"Hello\""], + "readback": "plain text content of the textbox", + "enforcement": "strict" + }, + "font": { + "type": "string", + "description": "font family. Bare 'font' targets Latin + EastAsian; for per-script control (Japanese / Korean / Arabic) use font.latin, font.ea, or font.cs.", + "aliases": ["fontname", "fontFamily"], + "add": true, "set": true, "get": true, + "examples": ["--prop font=Calibri"], + "readback": "font family string", + "enforcement": "report" + }, + "font.latin": { + "type": "string", + "description": "Latin-script font slot (a:latin) only.", + "add": true, "set": true, "get": true, + "examples": ["--prop font.latin=Calibri"], + "enforcement": "report" + }, + "font.ea": { + "type": "string", + "description": "East-Asian font slot (a:ea) — Chinese / Japanese / Korean text.", + "aliases": ["font.eastasia", "font.eastasian"], + "add": true, "set": true, "get": true, + "examples": ["--prop font.ea=\"メイリオ\""], + "enforcement": "report" + }, + "font.cs": { + "type": "string", + "description": "Complex-script font slot (a:cs) — Arabic / Hebrew / Thai etc.", + "aliases": ["font.complexscript", "font.complex"], + "add": true, "set": true, "get": true, + "examples": ["--prop font.cs=\"Arabic Typesetting\""], + "enforcement": "report" + }, + "direction": { + "type": "enum", + "values": ["ltr", "rtl"], + "description": "paragraph reading direction (a:pPr rtl). Use 'rtl' for Arabic / Hebrew layouts.", + "aliases": ["dir", "rtl"], + "add": true, "set": true, "get": true, + "examples": ["--prop direction=rtl"], + "enforcement": "report" + }, + "size": { + "type": "font-size", + "description": "font size — Add/Set forwards to the first run; Get exposes the inheritance-resolved value as effective.size, not bare size.", + "aliases": ["fontsize"], + "add": true, "set": true, "get": false, + "examples": ["--prop size=14", "--prop size=14pt", "--prop size=10.5pt"], + "readback": "n/a at shape level — read effective.size on the shape, or size on the inner run", + "enforcement": "strict" + }, + "bold": { + "type": "bool", + "description": "Add/Set forwards to the first run; Get does not surface bold at the shape level.", + "add": true, "set": true, "get": false, + "examples": ["--prop bold=true"], + "readback": "n/a at shape level — read bold on the inner run", + "enforcement": "strict" + }, + "italic": { + "type": "bool", + "description": "Add/Set forwards to the first run; Get does not surface italic at the shape level.", + "add": true, "set": true, "get": false, + "examples": ["--prop italic=true"], + "readback": "n/a at shape level — read italic on the inner run", + "enforcement": "strict" + }, + "color": { + "type": "color", + "description": "text color — Add/Set forwards to the first run; Get exposes the inheritance-resolved value as effective.color, not bare color.", + "add": true, "set": true, "get": false, + "examples": ["--prop color=0000FF", "--prop color=#0000FF"], + "readback": "n/a at shape level — read effective.color on the shape, or color on the inner run", + "enforcement": "strict" + }, + "fill": { + "type": "color", + "description": "textbox background fill", + "aliases": ["background"], + "add": true, "set": true, "get": true, + "examples": ["--prop fill=FFFF00", "--prop fill=accent1"], + "readback": "#RRGGBB (uppercase) or scheme color name", + "enforcement": "strict" + }, + "align": { + "type": "enum", + "values": ["left", "center", "right", "justify"], + "description": "text horizontal alignment", + "add": true, "set": true, "get": true, + "examples": ["--prop align=center"], + "readback": "one of: left | center | right | justify", + "enforcement": "strict" + }, + "width": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop width=5cm"], + "readback": "length in cm", + "enforcement": "strict" + }, + "height": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop height=3cm"], + "readback": "length in cm", + "enforcement": "strict" + }, + "x": { + "type": "length", + "description": "horizontal position of the textbox", + "add": true, "set": true, "get": true, + "examples": ["--prop x=2cm", "--prop x=1in", "--prop x=72pt"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "strict" + }, + "y": { + "type": "length", + "description": "vertical position of the textbox", + "add": true, "set": true, "get": true, + "examples": ["--prop y=3cm"], + "readback": "length in cm (e.g. \"3cm\")", + "enforcement": "strict" + }, + "effective.direction": { + "type": "enum", + "values": ["rtl", "ltr"], + "description": "resolved reading direction inherited from placeholder→layout→master→presentation defaults. Suppressed when 'direction' is set directly on the textbox.", + "add": false, "set": false, "get": true, + "readback": "rtl | ltr", + "enforcement": "report" + }, + "effective.size": { + "type": "length", + "description": "resolved font size inherited from placeholder→layout→master→presentation defaults. Suppressed when 'size' is set directly on the textbox.", + "add": false, "set": false, "get": true, + "readback": "unit-qualified pt (e.g. \"18pt\")", + "enforcement": "report" + }, + "effective.font": { + "type": "string", + "description": "resolved font name inherited from placeholder→layout→master→presentation defaults. Suppressed when 'font' is set directly on the textbox.", + "add": false, "set": false, "get": true, + "readback": "font name", + "enforcement": "report" + }, + "effective.color": { + "type": "color", + "description": "resolved text color inherited from placeholder→layout→master→presentation defaults. Suppressed when 'color' is set directly on the textbox.", + "add": false, "set": false, "get": true, + "readback": "#-prefixed uppercase hex (scheme colors pass through)", + "enforcement": "report" + }, + "effective.bold": { + "type": "bool", + "description": "resolved bold inherited from placeholder→layout→master→presentation defaults. Suppressed when 'bold' is set directly on the textbox.", + "add": false, "set": false, "get": true, + "readback": "true/false", + "enforcement": "report" + }, + "autoFit": { + "type": "enum", + "description": "auto-fit mode for the textbox text body. Alias: autofit.", + "values": ["none", "normal", "shape", "noAutofit", "spAutoFit"], + "aliases": ["autofit"], + "add": true, "set": true, "get": true, + "examples": ["--prop autoFit=shape"], + "readback": "one of: none | normal | shape", + "enforcement": "report" + }, + "valign": { + "type": "enum", + "values": ["top", "center", "middle", "bottom"], + "description": "text vertical anchoring inside the textbox (a:bodyPr anchor). 'middle' is an input alias for 'center' — Get always reads back 'center'.", + "add": true, "set": true, "get": true, + "examples": ["--prop valign=middle", "--prop valign=bottom"], + "readback": "one of: top | center | bottom", + "enforcement": "strict" + }, + "margin": { + "type": "length", + "description": "text body insets (a:bodyPr lIns/tIns/rIns/bIns). Single value applies to all four sides; CSS-style 4-value form is 'lIns,tIns,rIns,bIns'. '0' / '0cm' is a valid round-trip and is preserved (not collapsed to default).", + "add": true, "set": true, "get": true, + "examples": ["--prop margin=0.25cm", "--prop margin=0.2cm,0.5cm,0.2cm,0.5cm", "--prop margin=0"], + "readback": "single length (e.g. \"0.25cm\") when all four sides match, otherwise comma-separated 4-tuple", + "enforcement": "strict" + }, + "id": { + "type": "number", + "description": "OOXML shape id; source of the @id in the stable path /shape[@id=ID].", + "add": false, "set": false, "get": true, + "readback": "integer shape id", + "enforcement": "report" + }, + "zorder": { + "type": "number", + "description": "1-based z-order in slide shape tree. On add/set, repositions the shape within the shape tree (1 = back).", + "aliases": ["z-order", "order"], + "add": true, "set": true, "get": true, + "examples": ["--prop zorder=1"], + "readback": "1-based integer (1 = back)", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "Override the auto-generated 'TextBox {N}' label on cNvPr @name.", + "add": true, "set": true, "get": true, + "examples": ["--prop name=\"banner\""], + "readback": "shape name string (cNvPr @name)", + "enforcement": "report" + }, + "lang": { + "type": "string", + "description": "BCP-47 language tag on first run rPr (drawingML rPr/@lang).", + "aliases": ["altLang", "altlang"], + "add": true, "set": true, "get": true, + "examples": ["--prop lang=en-US"], + "readback": "BCP-47 language tag (e.g. \"en-US\")", + "enforcement": "report" + } + } +} diff --git a/schemas/help/pptx/theme.json b/schemas/help/pptx/theme.json new file mode 100644 index 000000000..71a837e44 --- /dev/null +++ b/schemas/help/pptx/theme.json @@ -0,0 +1,142 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "theme", + "parent": "presentation", + "container": true, + "operations": { + "add": false, + "set": true, + "get": true, + "query": true, + "remove": false + }, + "paths": { + "positional": ["/theme"] + }, + "note": "Presentation theme part — fonts and color scheme. Set accepts a limited subset (color scheme entries, heading/body font); full theme replacement is not supported.", + "properties": { + "headingFont": { + "type": "string", + "description": "major (heading) Latin typeface.", + "aliases": ["majorFont", "majorfont", "major"], + "add": false, "set": true, "get": true, + "examples": ["--prop headingFont=\"Calibri Light\""], + "readback": "font name", + "enforcement": "report" + }, + "bodyFont": { + "type": "string", + "description": "minor (body) Latin typeface.", + "aliases": ["minorFont", "minorfont", "minor"], + "add": false, "set": true, "get": true, + "examples": ["--prop bodyFont=Calibri"], + "readback": "font name", + "enforcement": "report" + }, + "headingFont.ea": { + "type": "string", + "description": "major (heading) East Asian typeface (CJK).", + "aliases": ["majorFont.ea", "majorfont.ea"], + "add": false, "set": true, "get": true, + "examples": ["--prop headingFont.ea=\"Yu Gothic\""], + "readback": "font name", + "enforcement": "report" + }, + "headingFont.cs": { + "type": "string", + "description": "major (heading) Complex Script typeface (Arabic/Hebrew/Thai).", + "aliases": ["majorFont.cs", "majorfont.cs"], + "add": false, "set": true, "get": true, + "examples": ["--prop headingFont.cs=Arial"], + "readback": "font name", + "enforcement": "report" + }, + "bodyFont.ea": { + "type": "string", + "description": "minor (body) East Asian typeface (CJK).", + "aliases": ["minorFont.ea", "minorfont.ea"], + "add": false, "set": true, "get": true, + "examples": ["--prop bodyFont.ea=\"Yu Gothic\""], + "readback": "font name", + "enforcement": "report" + }, + "bodyFont.cs": { + "type": "string", + "description": "minor (body) Complex Script typeface (Arabic/Hebrew/Thai).", + "aliases": ["minorFont.cs", "minorfont.cs"], + "add": false, "set": true, "get": true, + "examples": ["--prop bodyFont.cs=\"Times New Roman\""], + "readback": "font name", + "enforcement": "report" + }, + "dk1": { + "type": "color", + "description": "dark 1 — default text color in the theme color scheme.", + "aliases": ["dark1"], + "add": false, "set": true, "get": true, + "examples": ["--prop dk1=#000000"], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "lt1": { + "type": "color", + "description": "light 1 — default background color in the theme color scheme.", + "aliases": ["light1"], + "add": false, "set": true, "get": true, + "examples": ["--prop lt1=#FFFFFF"], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "dk2": { + "type": "color", + "description": "dark 2 — secondary dark / dark accent color in the theme color scheme.", + "aliases": ["dark2"], + "add": false, "set": true, "get": true, + "examples": ["--prop dk2=#44546A"], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "lt2": { + "type": "color", + "description": "light 2 — secondary light / light accent color in the theme color scheme.", + "aliases": ["light2"], + "add": false, "set": true, "get": true, + "examples": ["--prop lt2=#E7E6E6"], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "accent1": { + "type": "color", + "description": "theme accent color 1.", + "add": false, "set": true, "get": true, + "examples": ["--prop accent1=#4472C4"], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "accent2": { "type": "color", "description": "theme accent color 2.", "add": false, "set": true, "get": true, "examples": ["--prop accent2=#ED7D31"], "readback": "#-prefixed uppercase hex", "enforcement": "report" }, + "accent3": { "type": "color", "description": "theme accent color 3.", "add": false, "set": true, "get": true, "examples": ["--prop accent3=#A5A5A5"], "readback": "#-prefixed uppercase hex", "enforcement": "report" }, + "accent4": { "type": "color", "description": "theme accent color 4.", "add": false, "set": true, "get": true, "examples": ["--prop accent4=#FFC000"], "readback": "#-prefixed uppercase hex", "enforcement": "report" }, + "accent5": { "type": "color", "description": "theme accent color 5.", "add": false, "set": true, "get": true, "examples": ["--prop accent5=#5B9BD5"], "readback": "#-prefixed uppercase hex", "enforcement": "report" }, + "accent6": { "type": "color", "description": "theme accent color 6.", "add": false, "set": true, "get": true, "examples": ["--prop accent6=#70AD47"], "readback": "#-prefixed uppercase hex", "enforcement": "report" }, + "hyperlink": { + "type": "color", + "description": "theme hyperlink color.", + "aliases": ["hlink"], + "add": false, "set": true, "get": true, + "examples": ["--prop hyperlink=#0563C1"], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "followedhyperlink": { + "type": "color", + "description": "theme followed (visited) hyperlink color.", + "aliases": ["folhlink"], + "add": false, "set": true, "get": true, + "examples": ["--prop followedhyperlink=#954F72"], + "readback": "#-prefixed uppercase hex", + "enforcement": "report" + }, + "name": { "type":"string", "add":false, "set":true, "get":true, "description":"theme color scheme name (e.g. 'Office'). Set persists clrScheme/@name across reopen.", "examples":["--prop name=Custom"], "readback":"scheme name string", "enforcement":"report" } + } +} diff --git a/schemas/help/pptx/transition.json b/schemas/help/pptx/transition.json new file mode 100644 index 000000000..0c0b37e79 --- /dev/null +++ b/schemas/help/pptx/transition.json @@ -0,0 +1,16 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "transition", + "parent": "slide", + "operations": {"set": true, "get": true}, + "paths": {"positional": ["/slide[N]"]}, + "note": "Slide-level transition properties. Set/Get target the slide node itself; no independent child path. Set examples: `set /slide[1] --prop transition=morph --prop advanceTime=3000`. The `transition` value also accepts a combined shorthand 'TYPE[-DIR][-SPEED|DUR]' (e.g. `transition=push-right-1500`, `transition=fade-slow`) — this is the only path to set transitionDuration/transitionSpeed/direction. Lookup: Set.Slide.cs:286/293/297; Get: Animations.cs:1346/1358/1408.", + "properties": { + "transition": { "type":"enum", "values":["none","morph","fade","cut","dissolve","circle","diamond","newsflash","plus","random","wedge","wipe","push","cover","pull","uncover","wheel","zoom","box","split","blinds","venetian","checker","checkerboard","comb","bars","randombar","strips","diagonal","flash","honeycomb","vortex","switch","flip","ripple","glitter","prism","cube","rotate","orbit","clock","doors","window","shred","ferris","flythrough","warp","gallery","conveyor","pan","reveal","fallOver","drape","curtains","wind","prestige","fracture","crush","peelOff","pageCurlDouble","pageCurlSingle","airplane","origami"], "set":true, "get":true, "description":"transition type token, optionally combined with direction/speed/duration via 'TYPE-DIR-SPEED' or 'TYPE-DIR-DUR' (e.g. push-right-1500, split-vertical-in, fade-slow). 'none' removes the transition.", "readback":"canonical 'TYPE' or 'TYPE-DIR' token (speed/duration round-trip via transitionSpeed/transitionDuration)", "examples":["--prop transition=morph","--prop transition=push-right","--prop transition=split-vertical-in","--prop transition=fade-slow","--prop transition=none"], "enforcement":"report" }, + "advanceTime": { "type":"string", "set":true, "get":true, "description":"auto-advance after time (ms, or 'none' to clear)", "readback":"ms string", "examples":["--prop advanceTime=3000","--prop advanceTime=none"], "enforcement":"report" }, + "advanceClick": { "type":"bool", "set":true, "get":true, "description":"advance on click (schema default true; attribute is stripped when set to true)", "readback":"true | false", "examples":["--prop advanceClick=false"], "enforcement":"report" }, + "transitionDuration": { "type":"number", "set":false, "get":true, "description":"transition duration in milliseconds (CT_TransitionStartSoundAction @dur on PowerPoint 2010+ extLst transition). Set indirectly via the combined transition shorthand: `transition=TYPE[-DIR]-DUR_MS` (e.g. `transition=push-right-1500`).", "readback":"integer ms", "enforcement":"report" }, + "transitionSpeed": { "type":"enum", "values":["fast","med","slow"], "set":false, "get":true, "description":"legacy transition speed token (CT_SlideTransition @spd) — fast/med/slow. Set indirectly via the combined transition shorthand: `transition=TYPE[-DIR]-SPEED` (e.g. `transition=fade-slow`).", "readback":"speed token", "enforcement":"report" } + } +} diff --git a/schemas/help/pptx/zoom.json b/schemas/help/pptx/zoom.json new file mode 100644 index 000000000..af3784ae0 --- /dev/null +++ b/schemas/help/pptx/zoom.json @@ -0,0 +1,82 @@ +{ + "$schema": "../_schema.json", + "format": "pptx", + "element": "zoom", + "parent": "slide", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/slide[N]/zoom[M]"] + }, + "note": "Aliases: slidezoom, slide-zoom. Creates a slide-zoom link on the source slide pointing to target slide. Default size 8cm × 4.5cm centered. Used for interactive non-linear navigation.", + "properties": { + "target": { + "type": "int", + "description": "target slide number (1-based). Required. Alias: slide.", + "aliases": ["slide"], + "add": true, "set": true, "get": true, + "examples": ["--prop target=3"], + "readback": "target slide index", + "enforcement": "report" + }, + "x": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop x=2cm"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "y": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop y=2cm"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "width": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop width=8cm"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "height": { + "type": "length", + "add": true, "set": true, "get": true, + "examples": ["--prop height=4.5cm"], + "readback": "length in cm (e.g. \"2cm\")", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "zoom frame name. Defaults to 'Slide Zoom N'.", + "add": true, "set": true, "get": true, + "examples": ["--prop name=\"Section 2\""], + "readback": "name string", + "enforcement": "report" + }, + "returnToParent": { + "type": "bool", + "description": "return to parent slide after zoom plays.", + "aliases": ["returntoparent"], + "add": true, "set": true, "get": true, + "examples": ["--prop returnToParent=true"], + "readback": "true/false", + "enforcement": "report" + }, + "transitionDur": { + "type": "int", + "description": "transition duration in ms. Defaults to 1000.", + "aliases": ["transitiondur"], + "add": true, "set": true, "get": true, + "examples": ["--prop transitionDur=1500"], + "readback": "ms", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/aboveaverage.json b/schemas/help/xlsx/aboveaverage.json new file mode 100644 index 000000000..40b504c01 --- /dev/null +++ b/schemas/help/xlsx/aboveaverage.json @@ -0,0 +1,21 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "aboveaverage", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Above/below-average rule. Add via `add /Sheet1/cf --type aboveaverage --prop sqref=A1:A100 --prop above=true`. Lookup: Add.Cf.cs:606 (AddCfExtended `aboveaverage` case); Get: Query.cs:555.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A100"], "enforcement":"report" }, + "aboveAverage": { "type":"bool", "aliases":["above", "aboveaverage"], "add":true, "get":true, "description":"highlight above-average values (default true). Set false for below-average.", "examples":["--prop aboveAverage=true","--prop aboveAverage=false"], "readback":"true | false", "enforcement":"report" }, + "stdDev": { "type":"number", "add":true, "get":false, "description":"standard-deviation count (1, 2, ...) above/below the mean.", "examples":["--prop stdDev=1"], "enforcement":"report" }, + "equalAverage": { "type":"bool", "add":true, "get":false, "description":"include cells equal to the mean.", "examples":["--prop equalAverage=true"], "enforcement":"report" }, + "fill": { "type":"color", "add":true, "get":false, "description":"background fill via dxf.", "examples":["--prop fill=FFFF00"], "enforcement":"report" }, + "font.color": { "type":"color", "add":true, "get":false, "description":"font color via dxf.", "examples":["--prop font.color=FF0000"], "enforcement":"report" }, + "font.bold": { "type":"bool", "add":true, "get":false, "description":"bold via dxf.", "examples":["--prop font.bold=true"], "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/autofilter.json b/schemas/help/xlsx/autofilter.json new file mode 100644 index 000000000..37d62a7dc --- /dev/null +++ b/schemas/help/xlsx/autofilter.json @@ -0,0 +1,36 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "autofilter", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/autofilter"] + }, + "note": "A sheet has at most one AutoFilter. Per-column criteria via 'criteriaN.OP=VAL' props where N is the 0-based column offset from the filter range and OP ∈ {equals, notEquals, contains, doesNotContain, beginsWith, endsWith, gt, gte, lt, lte, between, notBetween, top, topPercent, bottom, bottomPercent, blanks, nonBlanks, values, dynamic}. Canonical key matches sheet.autoFilter alias.", + "properties": { + "range": { + "type": "string", + "description": "cell range the filter applies to. Required.", + "aliases": ["ref"], + "add": true, "set": true, "get": true, + "examples": ["--prop range=A1:F100"], + "readback": "range reference", + "enforcement": "report" + }, + "criteria0": { + "type": "string", + "description": "column 0 filter criterion. Use dotted key: --prop criteria0.OP=VAL. OP ∈ equals/notEquals/contains/doesNotContain/beginsWith/endsWith/gt/gte/lt/lte/between/notBetween/top/topPercent/bottom/bottomPercent/blanks/nonBlanks/values/dynamic. Use criteria1, criteria2, … for additional columns. Add-time only — Set on /sheet/autofilter currently accepts only `range`, and Get does not surface stored criteria back today.", + "add": true, "set": false, "get": false, + "examples": ["--prop criteria0.equals=Red", "--prop criteria2.gt=100"], + "readback": "criterion spec", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/cell.json b/schemas/help/xlsx/cell.json new file mode 100644 index 000000000..2d2224b40 --- /dev/null +++ b/schemas/help/xlsx/cell.json @@ -0,0 +1,320 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "cell", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": ["//"], + "positional": ["/Sheet1/A1", "/Sheet1/B2:C3"] + }, + "note": "Get returns canonical keys: numberformat (not `format`), alignment.{horizontal,vertical,wrapText}, font.{bold,italic,name,size,color}. Add/Set accept both font.* and short forms (bold, italic, font, size). The bare `color` key is rejected on cells (ambiguous with `fill`) — use `font.color`. Parent path controls placement: `/Sheet1` appends to the next empty cell; `/Sheet1/A2` targets a specific cell. Set auto-vivifies any in-bounds cell (A1:XFD1048576) — writing to a never-written cell creates it rather than throwing not_found, since OOXML xlsx stores only cells with content. Sheet-level and out-of-bounds writes still error.", + "properties": { + "value": { + "type": "string", + "description": "literal cell value — string, number, or date. Numeric strings stored as numbers unless cell has text format (@) or explicit type=string. Readback in DocumentNode.Text.", + "add": true, "set": true, "get": true, + "examples": ["--prop value=\"Hello\"", "--prop value=42", "--prop value=42 --prop type=string", "--prop value=42 --prop type=number"], + "readback": "plain string in DocumentNode.Text", + "enforcement": "report" + }, + "formula": { + "type": "string", + "description": "cell formula, without leading =", + "add": true, "set": true, "get": true, + "examples": ["--prop formula=\"SUM(A1:A10)\""], + "readback": "formula text without leading =", + "enforcement": "report" + }, + "numberformat": { + "type": "string", + "description": "Excel format code — covers number, date, percentage, currency, and text (@). \"@\" forces String storage on subsequent values.", + "aliases": ["format", "numfmt"], + "add": true, "set": true, "get": true, + "examples": ["--prop numberformat=\"#,##0.00\"", "--prop numberformat=yyyy-mm-dd", "--prop numberformat=@"], + "readback": "format string as stored", + "enforcement": "report" + }, + "font.bold": { + "type": "bool", + "description": "bold font weight on the cell.", + "aliases": ["bold"], + "add": true, "set": true, "get": true, + "examples": ["--prop bold=true"], + "readback": "true | false", + "enforcement": "strict" + }, + "font.italic": { + "type": "bool", + "description": "italic style on the cell.", + "aliases": ["italic"], + "add": true, "set": true, "get": true, + "examples": ["--prop italic=true"], + "readback": "true | false", + "enforcement": "strict" + }, + "font.name": { + "type": "string", + "description": "font family name. Aliases: font, fontname.", + "aliases": ["font", "fontname"], + "add": true, "set": true, "get": true, + "examples": ["--prop font=\"Calibri\""], + "readback": "font family name string", + "enforcement": "strict" + }, + "font.size": { + "type": "font-size", + "aliases": ["size", "fontsize"], + "add": true, "set": true, "get": true, + "examples": ["--prop size=11pt"], + "readback": "unit-qualified, e.g. \"11pt\"", + "enforcement": "strict" + }, + "font.color": { + "type": "color", + "description": "font color on the cell. Note: the bare 'color' alias is intentionally rejected on cells due to ambiguity with 'fill' (background) — use 'font.color' explicitly.", + "add": true, "set": true, "get": true, + "examples": ["--prop font.color=FF0000"], + "readback": "#RRGGBB uppercase", + "enforcement": "report" + }, + "fill": { + "type": "color", + "description": "cell background fill. Solid color (hex / named / rgb(...)) or a linear gradient as 'COLOR1-COLOR2[-ANGLE]' / 'gradient;COLOR1;COLOR2[;ANGLE]'. Scheme color names (accent1..) and 'none' are not accepted on input — readback may surface them when a cell already carries them.", + "aliases": ["bgcolor"], + "add": true, "set": true, "get": true, + "examples": [ + "--prop fill=FFFF00", + "--prop fill=#FF0000", + "--prop fill=red", + "--prop fill=\"FF0000-0000FF-90\"", + "--prop fill=\"gradient;FF0000;0000FF;90\"" + ], + "readback": "#RRGGBB uppercase, 'gradient;#START;#END;ANGLE' for gradients, scheme color name (e.g. accent1) when set externally", + "enforcement": "report" + }, + "strike": { + "type": "bool", + "description": "single strikethrough on the cell text.", + "aliases": ["strikethrough", "font.strike"], + "add": true, "set": true, "get": true, + "examples": ["--prop strike=true"], + "readback": "true | false", + "enforcement": "report" + }, + "underline": { + "type": "enum", + "values": ["single", "double", "singleAccounting", "doubleAccounting", "none"], + "description": "underline style on the cell text.", + "aliases": ["font.underline"], + "add": true, "set": true, "get": true, + "examples": ["--prop underline=single"], + "readback": "underline style name", + "enforcement": "report" + }, + "locked": { + "type": "bool", + "description": "cell protection: lock the cell against edits when the sheet is protected. Default Excel behavior is locked=true. Add/Set only — readback is exposed under 'protection.locked'.", + "add": true, "set": true, "get": false, + "examples": ["--prop locked=false"], + "readback": "n/a (use protection.locked on Get)", + "enforcement": "report" + }, + "protection.locked": { + "type": "bool", + "description": "cell protection lock state. Get-only readback (dotted form). For Add/Set use the flat `locked` key.", + "add": false, "set": false, "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "protection.hidden": { + "type": "bool", + "description": "hide formula in protected sheet. Get-only readback.", + "add": false, "set": false, "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "numFmtId": { + "type": "number", + "description": "raw OOXML number format id (supplementary; prefer `numberformat`). Emitted only when numFmtId > 0.", + "add": false, "set": false, "get": true, + "readback": "integer", + "enforcement": "report" + }, + "phonetic": { + "type": "string", + "description": "phonetic guide text from SST PhoneticRun. Emitted only when present.", + "add": false, "set": false, "get": true, + "readback": "phonetic string", + "enforcement": "report" + }, + "quotePrefix": { + "type": "bool", + "description": "leading apostrophe quote-prefix flag. Emitted only when set.", + "add": false, "set": false, "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "alignment.horizontal": { + "type": "enum", + "values": ["left", "center", "right", "justify", "fill", "distributed"], + "description": "horizontal text alignment. Alias: halign.", + "aliases": ["halign"], + "add": true, "set": true, "get": true, + "examples": ["--prop alignment.horizontal=center"], + "readback": "one of values", + "enforcement": "report" + }, + "alignment.vertical": { + "type": "enum", + "values": ["top", "center", "bottom"], + "description": "vertical text alignment. Alias: valign.", + "aliases": ["valign"], + "add": true, "set": true, "get": true, + "examples": ["--prop alignment.vertical=center"], + "readback": "one of values", + "enforcement": "report" + }, + "alignment.wrapText": { + "type": "bool", + "description": "wrap text within the cell. Aliases: wrap, wrapText.", + "aliases": ["wrap", "wrapText"], + "add": true, "set": true, "get": true, + "examples": ["--prop alignment.wrapText=true"], + "readback": "true | false", + "enforcement": "report" + }, + "alignment.readingOrder": { + "type": "enum", + "values": ["context", "ltr", "rtl"], + "description": "cell text reading direction (OOXML 0=context, 1=ltr, 2=rtl). Use 'rtl' for Arabic / Hebrew, 'ltr' to force left-to-right, 'context' (default) to derive from content.", + "aliases": ["readingorder", "readingOrder", "direction", "dir"], + "add": true, "set": true, "get": true, + "examples": ["--prop alignment.readingOrder=rtl", "--prop direction=rtl"], + "enforcement": "report" + }, + "merge": { + "type": "string", + "description": "merge range applied post-cell-creation (parity with `set`). Accepts a single A1 range (A1:C3) or comma-separated ranges (A1:B1,A2:B2). Use merge=false to clear an existing merge anchored at this cell (aliases: unmerge, none, empty).", + "add": true, "set": true, "get": false, + "examples": ["--prop merge=A1:C3", "--prop merge=false"], + "enforcement": "report" + }, + "ref": { + "type": "string", + "description": "target A1 cell reference (alternative to encoding the address in the path tail).", + "aliases": ["address"], + "add": true, "set": false, "get": false, + "examples": ["--prop ref=B2"], + "enforcement": "report" + }, + "link": { + "type": "string", + "description": "hyperlink target attached to the cell. Accepts external URL (https://, http://, mailto:, file://, onenote:, tel:) or internal anchor (#Sheet!Cell, #NamedRange). Use link=none on Set to remove. Parity with Set.", + "add": true, "set": true, "get": true, + "examples": [ + "--prop link=https://example.com", + "--prop link=mailto:user@example.com", + "--prop link=#Sheet2!A1" + ], + "readback": "URL string or internal anchor as stored", + "enforcement": "report" + }, + "tooltip": { + "type": "string", + "description": "ScreenTip text shown on hover for an existing hyperlink. Pair with link= during Add, or apply to a cell that already has a hyperlink during Set.", + "aliases": ["screenTip", "screentip"], + "add": true, "set": true, "get": false, + "examples": ["--prop link=https://example.com --prop tooltip=\"Open in browser\""], + "enforcement": "report" + }, + "type": { + "type": "enum", + "values": ["string", "number", "boolean", "date", "error", "richtext"], + "description": "force a cell type. Normally inferred from value/formula. Add/Set accept the listed values only; SST-backed shared strings and inline strings are not creatable via Add (use plain string instead). Get may still surface 'SharedString' or 'InlineString' when reading cells written by Excel or other tools.", + "add": true, "set": true, "get": true, + "examples": ["--prop value=01234 --prop type=string"], + "readback": "PascalCase type name (e.g. \"String\", \"Number\", \"Boolean\", \"Error\", \"SharedString\", \"InlineString\", \"Date\")", + "enforcement": "report" + }, + "runs": { + "type": "string", + "description": "rich-text runs as JSON array (e.g. '[{\"text\":\"Hello\",\"bold\":true}]'). Used when type=richtext.", + "add": true, "set": false, "get": false, + "examples": ["--prop type=richtext --prop runs='[{\"text\":\"Bold\",\"bold\":true}]'"], + "readback": "n/a (decomposed into /run[N] subnodes)", + "enforcement": "report" + }, + "clear": { + "type": "bool", + "description": "clear the cell value/formula before applying new content.", + "add": true, "set": true, "get": false, + "examples": ["--prop clear=true"], + "readback": "n/a", + "enforcement": "report" + }, + "shift": { + "type": "string", + "description": "Cell-level Insert/Delete shift (Excel UI parity). On `add --type cell`: 'right' pushes existing cells in the same row right by one to make room; 'down' pushes existing cells in the same column down by one. On `remove` (passed via the top-level `--shift` flag, not `--prop`): 'left' shifts cells in the same row left into the gap; 'up' shifts cells in the same column up. Scope cap: only cellRefs within the affected row (left/right) or column (up/down) are rewritten — formulas, mergeCells, and CF/DV/hyperlink/table refs that span the affected band are NOT adjusted. For full-band shift with all metadata adjusted, use add/remove --type row or --type col.", + "add": true, "set": false, "get": false, + "examples": [ + "add file.xlsx /Sheet1/B5 --type cell --prop shift=right --prop value=NEW", + "add file.xlsx /Sheet1/B5 --type cell --prop shift=down --prop value=NEW", + "remove file.xlsx /Sheet1/B5 --shift left", + "remove file.xlsx /Sheet1/B5 --shift up" + ], + "readback": "n/a (structural)", + "enforcement": "report" + }, + "arrayformula": { + "type": "string", + "description": "dynamic-array formula spilled into ref range (without leading '=').", + "add": true, "set": true, "get": false, + "examples": ["--prop arrayformula=\"A1:A10*2\" --prop ref=B1:B10"], + "readback": "n/a", + "enforcement": "report" + }, + "cachedValue": { + "type": "string", + "description": "raw cached display value as stored in the file. Surfaces only on Get/Query for formula cells; absent on plain-value cells. Mirrors what the last writer (Excel/LibreOffice) computed — may be stale.", + "add": false, "set": false, "get": true, + "readback": "raw cached display value for formula cells; absent on plain-value cells", + "enforcement": "report" + }, + "computedValue": { + "type": "string", + "description": "OfficeCli evaluator's prediction of the formula result. Surfaces only on Get/Query for formula cells when the evaluator could compute a value. Compare against cachedValue to detect stale caches (see view issues subtype formula_cache_stale).", + "add": false, "set": false, "get": true, + "readback": "evaluator-predicted value for formula cells; absent when the evaluator could not compute", + "enforcement": "report" + }, + "evaluated": { + "type": "bool", + "description": "cross-handler protocol flag — true when cachedValue is present OR the evaluator produced computedValue; false when the formula has neither (view text substitutes #OCLI_NOTEVAL!). Read this via get --json instead of pattern-matching the sentinel, since literal user content may collide. Subtype: formula_not_evaluated.", + "add": false, "set": false, "get": true, + "readback": "true|false (true ⇒ either cachedValue or computedValue is available)", + "enforcement": "report" + }, + "alignment.indent": { "type":"number", "add":false, "set":false, "get":true, "description":"cell alignment indent units (CT_CellAlignment @indent). Use the flat `indent` key on Add/Set.", "readback":"integer indent units", "enforcement":"report" }, + "alignment.shrinkToFit": { "type":"bool", "add":false, "set":false, "get":true, "description":"cell alignment shrinkToFit flag.", "readback":"true|false", "enforcement":"report" }, + "alignment.textRotation": { "type":"number", "add":false, "set":false, "get":true, "description":"cell alignment text rotation in degrees (CT_CellAlignment @textRotation).", "readback":"integer degrees", "enforcement":"report" }, + "font.subscript": { "type":"bool", "add":false, "set":false, "get":true, "description":"font vertical-alignment subscript flag (legacy alias of cell-level `subscript`).", "readback":"true|false", "enforcement":"report" }, + "font.superscript": { "type":"bool", "add":false, "set":false, "get":true, "description":"font vertical-alignment superscript flag (legacy alias of cell-level `superscript`).", "readback":"true|false", "enforcement":"report" }, + "border.diagonal": { "type":"string", "add":false, "set":false, "get":true, "description":"diagonal border line style (CT_Border/diagonal @style — thin, medium, thick, dashed, etc.).", "readback":"line-style token", "enforcement":"report" }, + "border.diagonal.color": { "type":"color", "add":false, "set":false, "get":true, "description":"diagonal border color.", "readback":"#RRGGBB uppercase", "enforcement":"report" }, + "border.diagonalDown": { "type":"bool", "add":false, "set":false, "get":true, "description":"true when the cell shows a top-left → bottom-right diagonal border.", "readback":"true|false", "enforcement":"report" }, + "border.diagonalUp": { "type":"bool", "add":false, "set":false, "get":true, "description":"true when the cell shows a bottom-left → top-right diagonal border.", "readback":"true|false", "enforcement":"report" }, + "arrayref": { "type":"string", "add":false, "set":false, "get":true, "description":"array-formula spill range (CellFormula @ref). Surfaces on the master cell of an array formula.", "readback":"A1 range string", "enforcement":"report" }, + "mergeAnchor": { "type":"bool", "add":false, "set":false, "get":true, "description":"true when this cell is the top-left anchor of a merged range. Empty merged-region cells receive mergeAnchor=false; the anchor receives true.", "readback":"true|false", "enforcement":"report" }, + "empty": { "type":"bool", "add":false, "set":false, "get":true, "description":"true when the cell has neither display text nor a formula. Useful for distinguishing styled-but-empty cells from data cells.", "readback":"true|false", "enforcement":"report" }, + "richtext": { "type":"bool", "add":false, "set":false, "get":true, "description":"true when the cell stores rich-text runs (multi-format text). Surfaces alongside `runs` in Get output.", "readback":"true|false", "enforcement":"report" }, + "subscript": { "type":"bool", "add":false, "set":false, "get":true, "description":"cell-level run subscript flag (when cell is rich text with a single run).", "readback":"true|false", "enforcement":"report" }, + "superscript": { "type":"bool", "add":false, "set":false, "get":true, "description":"cell-level run superscript flag (when cell is rich text with a single run).", "readback":"true|false", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/cellis.json b/schemas/help/xlsx/cellis.json new file mode 100644 index 000000000..b4739d747 --- /dev/null +++ b/schemas/help/xlsx/cellis.json @@ -0,0 +1,21 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "cellis", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Compare cell value against literal/formula. Add via `add /Sheet1/cf --type cellis --prop sqref=A1:A10 --prop operator=greaterThan --prop value=50 --prop fill=FFFF00`. Lookup: Add.Cf.cs:453 (AddCellIs); Get: Query.cs:585.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A10"], "enforcement":"report" }, + "operator": { "type":"enum", "values":["greaterThan","lessThan","greaterThanOrEqual","lessThanOrEqual","equal","notEqual","between","notBetween"], "add":true, "get":true, "description":"comparison operator. Aliases: gt/lt/gte/lte/eq/ne/=, etc.", "examples":["--prop operator=greaterThan","--prop operator=between"], "readback":"OOXML operator token", "enforcement":"report" }, + "value": { "type":"string", "aliases":["formula","value1"], "add":true, "get":true, "description":"primary comparison value (literal or formula). Required.", "examples":["--prop value=50","--prop value=\"=AVERAGE(A:A)\""], "readback":"formula text as stored", "enforcement":"report" }, + "value2": { "type":"string", "aliases":["formula2","maxvalue"], "add":true, "get":true, "description":"secondary value, only used by between/notBetween.", "examples":["--prop value2=100"], "readback":"formula text as stored", "enforcement":"report" }, + "fill": { "type":"color", "add":true, "get":false, "description":"background fill via dxf.", "examples":["--prop fill=FFFF00"], "enforcement":"report" }, + "font.color": { "type":"color", "add":true, "get":false, "description":"font color via dxf.", "examples":["--prop font.color=FF0000"], "enforcement":"report" }, + "font.bold": { "type":"bool", "add":true, "get":false, "description":"bold via dxf.", "examples":["--prop font.bold=true"], "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/cfextended.json b/schemas/help/xlsx/cfextended.json new file mode 100644 index 000000000..396e8a1b6 --- /dev/null +++ b/schemas/help/xlsx/cfextended.json @@ -0,0 +1,20 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "cfextended", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Catch-all CF dispatch for sub-types not exposed by their own --type alias: belowAverage, containsBlanks, notContainsBlanks, containsErrors, notContainsErrors, contains, notContains, beginsWith, endsWith. Pass `--prop type=` to select. Lookup: Add.Cf.cs:557 (AddCfExtended). Most subtypes share Query.cs readback through `cfType` (Query.cs:464+).", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A100"], "enforcement":"report" }, + "type": { "type":"enum", "values":["belowAverage","containsBlanks","notContainsBlanks","containsErrors","notContainsErrors","contains","notContains","beginsWith","endsWith"], "add":true, "get":false, "description":"subtype selector. Required.", "examples":["--prop type=containsBlanks","--prop type=beginsWith"], "enforcement":"report" }, + "text": { "type":"string", "add":true, "get":true, "description":"substring for contains/notContains/beginsWith/endsWith subtypes.", "examples":["--prop text=error"], "readback":"matched substring (when subtype emits it)", "enforcement":"report" }, + "fill": { "type":"color", "add":true, "get":false, "description":"background fill via dxf.", "examples":["--prop fill=FFCCCC"], "enforcement":"report" }, + "font.color": { "type":"color", "add":true, "get":false, "description":"font color via dxf.", "examples":["--prop font.color=FF0000"], "enforcement":"report" }, + "font.bold": { "type":"bool", "add":true, "get":false, "description":"bold via dxf.", "examples":["--prop font.bold=true"], "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/chart-axis.json b/schemas/help/xlsx/chart-axis.json new file mode 100644 index 000000000..be1a95fe7 --- /dev/null +++ b/schemas/help/xlsx/chart-axis.json @@ -0,0 +1,38 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "chart-axis", + "parent": "chart", + "operations": { + "add": false, + "set": true, + "get": true, + "remove": false + }, + "note": "Axes are created/destroyed implicitly by chartType changes — no direct Add/Remove. Extended charts (funnel/treemap/sunburst/boxWhisker/histogram) reject axis paths; use chart-level Set. At chart-creation time, configure axes via the chart's axis* props (axismin/axismax/axistitle/axisfont/…); chart-axis covers post-creation only. Known gaps: `labelFont` writes the title run (not tick labels); `lineWidth`/`lineDash` Set on an axis path applies to all plot-area series — use chart-series for series-specific line styling.", + "addressing": { + "key": "role", + "pathForm": "/SheetName/chart[N]/axis[@role=ROLE]", + "keyValues": [ + "category", + "value", + "value2", + "series" + ] + }, + "extends": [ + "_shared/chart-axis", + "_shared/chart-axis.pptx-xlsx" + ], + "properties": { + "role": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "axis role token — value, value2, category, series. Surfaces on Get to identify which axis this node represents.", + "readback": "axis role token (lowercase)", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/chart-series.json b/schemas/help/xlsx/chart-series.json new file mode 100644 index 000000000..3179f7779 --- /dev/null +++ b/schemas/help/xlsx/chart-series.json @@ -0,0 +1,51 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "chart-series", + "parent": "chart", + "operations": { + "add": true, + "set": true, + "get": true, + "remove": true + }, + "paths": { + "positional": [ + "/SheetName/chart[N]/series[K]" + ] + }, + "note": "At Add time, series pass as dotted props on the parent chart (series1.name, series1.values, series1.color, series1.categories); this schema is per-series Set/Get after creation. Combo charts (mixed chartType / secondary axis) are unsupported — create separate charts. `lineStyle` is not a key (rejected as UNSUPPORTED — use lineWidth + lineDash).", + "extends": [ + "_shared/chart-series", + "_shared/chart-series.pptx-xlsx" + ], + "properties": { + "valuesRef": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "A1 cell range backing the series values.", + "readback": "A1 range string", + "enforcement": "report" + }, + "trendline.dispEq": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "trendline displayEquation flag.", + "readback": "true when shown", + "enforcement": "report" + }, + "trendline.dispRSqr": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "trendline displayRSquaredValue flag.", + "readback": "true when shown", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/chart.json b/schemas/help/xlsx/chart.json new file mode 100644 index 000000000..31f8aa853 --- /dev/null +++ b/schemas/help/xlsx/chart.json @@ -0,0 +1,104 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "chart", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/SheetName/chart[N]" + ] + }, + "note": "Validator accepts dotted (axis./series./trendline./datalabels./font./fill./border./… ) and indexed (series{N}./dataLabel{N}./point{N}.) sub-prop namespaces in addition to keys declared below. Chart-level axis* props (axismin, axismax, axistitle, axisfont, …) are Add-time only; post-creation axis Set/Get goes through chart-axis.", + "extends": [ + "_shared/chart", + "_shared/chart.docx-xlsx", + "_shared/chart.pptx-xlsx" + ], + "properties": { + "radarStyle": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "radar chart style token (standard | marker | filled).", + "readback": "radar style token", + "enforcement": "report", + "aliases": [ + "radarstyle" + ], + "examples": [ + "--prop radarstyle=filled" + ], + "appliesWhen": { + "chartType": [ + "radar" + ] + } + }, + "roundedCorners": { + "type": "string", + "add": true, + "set": true, + "get": true, + "description": "chartSpace roundedCorners flag (true|false).", + "readback": "true|false", + "enforcement": "report", + "aliases": [ + "roundedcorners" + ], + "examples": [ + "--prop roundedcorners=true" + ] + }, + "valAxisVisible": { + "type": "bool", + "add": true, + "set": true, + "get": true, + "description": "convenience shortcut for /chart[N]/axis[@role=...] visible (on role=value); see chart-axis schema for full axis-level options", + "readback": "true|false", + "enforcement": "report", + "aliases": [ + "valaxis.visible", + "valaxisvisible" + ], + "examples": [ + "--prop valaxisvisible=false" + ] + }, + "view3d.perspective": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "3-D chart perspective (0..240).", + "readback": "integer perspective", + "enforcement": "report" + }, + "view3d.rotateX": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "3-D chart X rotation in degrees (-90..90).", + "readback": "integer degrees", + "enforcement": "report" + }, + "view3d.rotateY": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "3-D chart Y rotation in degrees (0..360).", + "readback": "integer degrees", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/colbreak.json b/schemas/help/xlsx/colbreak.json new file mode 100644 index 000000000..b1be1a29b --- /dev/null +++ b/schemas/help/xlsx/colbreak.json @@ -0,0 +1,28 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "colbreak", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/colbreak[N]"] + }, + "note": "Manual page break before the specified column. Accepts numeric index or column letter.", + "properties": { + "col": { + "type": "string", + "description": "column index or letter. Aliases: column, index.", + "aliases": ["column", "index"], + "add": true, "set": false, "get": false, + "examples": ["--prop col=F"], + "readback": "n/a (see sheet.colBreaks)", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/colorscale.json b/schemas/help/xlsx/colorscale.json new file mode 100644 index 000000000..b8cf81275 --- /dev/null +++ b/schemas/help/xlsx/colorscale.json @@ -0,0 +1,19 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "colorscale", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Conditional formatting 2-/3-stop color scale. Add via `add /Sheet1/cf --type colorscale --prop sqref=A1:A10 --prop mincolor=F8696B --prop maxcolor=63BE7B`. Lookup: Add.Cf.cs:266 (AddColorScale); Get: Query.cs:500.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A10"], "enforcement":"report" }, + "minColor": { "type":"color", "aliases":["mincolor"], "add":true, "get":true, "description":"low-end color (default F8696B).", "examples":["--prop minColor=F8696B"], "readback":"#-prefixed uppercase hex", "enforcement":"report" }, + "maxColor": { "type":"color", "aliases":["maxcolor"], "add":true, "get":true, "description":"high-end color (default 63BE7B).", "examples":["--prop maxColor=63BE7B"], "readback":"#-prefixed uppercase hex", "enforcement":"report" }, + "midColor": { "type":"color", "aliases":["midcolor"], "add":true, "get":true, "description":"midpoint color (omit for 2-stop scale).", "examples":["--prop midColor=FFEB84"], "readback":"#-prefixed uppercase hex", "enforcement":"report" }, + "midpoint": { "type":"number", "aliases":["midPoint"], "add":true, "get":false, "description":"midpoint percentile (default 50). Only meaningful when midcolor is set.", "examples":["--prop midpoint=50"], "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/column.json b/schemas/help/xlsx/column.json new file mode 100644 index 000000000..a7b6c6b81 --- /dev/null +++ b/schemas/help/xlsx/column.json @@ -0,0 +1,76 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "column", + "elementAliases": ["col"], + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/col[X]"] + }, + "note": "X is a column letter (A,B,…) or 1-based index. Insert/move/clone at an occupied slot shifts every column at or past the slot right by one, and every range-bearing structure on the sheet (mergeCells, CF/DV sqref, autoFilter, hyperlink/table refs, named ranges, formula cell-refs) follows the displacement. 'add --from /SheetName/col[L]' with --before/--after clones cells + single-col mergeCells; relative formula refs are delta-shifted to the new anchor (Excel 'Insert Copied Cells' parity).", + "properties": { + "name": { + "type": "string", + "description": "column letter to insert at (e.g. 'C'). If omitted, uses index position or appends.", + "add": true, "set": false, "get": false, + "examples": ["--prop name=C"], + "readback": "n/a (used only for addressing)", + "enforcement": "strict" + }, + "width": { + "type": "length", + "description": "column width in Excel character units (parsed by ParseColWidthChars). Accepts bare number or unit-qualified.", + "add": true, "set": true, "get": true, + "examples": ["--prop width=20"], + "readback": "raw double (character units)", + "enforcement": "strict" + }, + "hidden": { + "type": "bool", + "description": "hide column.", + "add": true, "set": true, "get": true, + "examples": ["--prop hidden=true"], + "readback": "true when hidden, key absent otherwise", + "enforcement": "strict" + }, + "outline": { + "type": "int", + "description": "outline/group level 0-7. Currently Set-only. Aliases: outlineLevel, group.", + "aliases": ["outlinelevel", "group"], + "add": false, "set": true, "get": false, + "examples": ["--prop outline=1"], + "readback": "not surfaced by Get", + "enforcement": "report" + }, + "collapsed": { + "type": "bool", + "description": "collapse column group. Currently Set-only.", + "add": false, "set": true, "get": false, + "examples": ["--prop collapsed=true"], + "readback": "not surfaced by Get", + "enforcement": "report" + }, + "customWidth": { + "type": "bool", + "description": "Get-only readback. True when the column has an explicit width set (i.e. width is not the sheet default). Mirrors OOXML col@customWidth.", + "add": false, "set": false, "get": true, + "readback": "true when column has explicit width", + "enforcement": "report" + }, + "autofit": { + "type": "bool", + "description": "auto-fit width to cell content. Set-only by design (meaningless at Add since new column has no data).", + "add": false, "set": true, "get": false, + "examples": ["--prop autofit=true"], + "readback": "resolves to width at Set time", + "enforcement": "strict" + } + } +} diff --git a/schemas/help/xlsx/comment.json b/schemas/help/xlsx/comment.json new file mode 100644 index 000000000..df9cf3516 --- /dev/null +++ b/schemas/help/xlsx/comment.json @@ -0,0 +1,35 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "comment", + "parent": "sheet|cell", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/SheetName/comment[N]", + "/SheetName/CellRef/comment" + ] + }, + "note": "Aliases: note. Anchored to a cell via 'ref' (or path tail). Modern Excel also supports threaded comments; this handler emits classic comments.", + "extends": "_shared/comment", + "properties": { + "ref": { + "type": "string", + "description": "target cell address (e.g. B2).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop ref=B2" + ], + "readback": "cell reference", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/conditionalformatting.json b/schemas/help/xlsx/conditionalformatting.json new file mode 100644 index 000000000..8653432de --- /dev/null +++ b/schemas/help/xlsx/conditionalformatting.json @@ -0,0 +1,273 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "conditionalformatting", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/cf[N]"] + }, + "note": "Aliases: cf, cfextended. Type names map to xlsx CF rules (cellIs, colorScale, dataBar, iconSet, containsText, top/bottom N, etc.).", + "properties": { + "type": { + "type": "enum", + "description": "CF rule type.", + "values": ["cellIs", "colorScale", "dataBar", "iconSet", "containsText", "notContainsText", "beginsWith", "endsWith", "top", "topN", "top10", "topPercent", "bottom", "bottomN", "bottomPercent", "aboveAverage", "belowAverage", "duplicateValues", "uniqueValues", "containsBlanks", "containsErrors", "notContainsBlanks", "notContainsErrors", "formula", "dateOccurring", "today", "yesterday", "tomorrow", "thisWeek", "lastWeek", "nextWeek", "thisMonth", "lastMonth", "nextMonth"], + "aliases": ["rule"], + "add": true, "set": true, "get": true, + "examples": ["--prop type=cellIs", "--prop rule=top10"], + "readback": "rule type", + "enforcement": "report" + }, + "ref": { + "type": "string", + "description": "target cell range.", + "aliases": ["range", "sqref"], + "add": true, "set": true, "get": true, + "examples": ["--prop ref=A1:A10", "--prop sqref=A1:A10"], + "readback": "cell range", + "enforcement": "report" + }, + "fill": { + "type": "color", + "description": "background fill color for matched cells. Use this for cellIs, text, top/bottom, and formula rules.", + "add": true, "set": true, "get": true, + "examples": ["--prop fill=FFFF00"], + "readback": "#-prefixed hex", + "enforcement": "report" + }, + "operator": { + "type": "string", + "description": "operator for cellIs/text rules.", + "add": true, "set": true, "get": true, + "examples": ["--prop operator=greaterThan"], + "readback": "operator", + "enforcement": "report" + }, + "value": { + "type": "string", + "description": "threshold / comparison value.", + "aliases": ["formula1", "formula"], + "add": true, "set": true, "get": true, + "examples": ["--prop value=100", "--prop formula=$A1>5"], + "readback": "value/formula", + "enforcement": "report" + }, + "color": { + "type": "color", + "description": "bar color for dataBar rules only. For cellIs/text/formula rules, use 'fill' instead.", + "add": true, "set": true, "get": true, + "examples": ["--prop color=#FFFF00"], + "readback": "#-prefixed hex", + "enforcement": "report" + }, + "value2": { + "type": "string", + "description": "second threshold for between/notBetween cellIs rules. Alias: maxvalue.", + "aliases": ["maxvalue"], + "add": true, "set": true, "get": true, + "examples": ["--prop value=10 --prop value2=20"], + "readback": "value/formula", + "enforcement": "report" + }, + "text": { + "type": "string", + "description": "needle for containsText/notContainsText/beginsWith/endsWith rules.", + "add": true, "set": true, "get": true, + "examples": ["--prop type=containsText --prop text=ERROR"], + "readback": "needle string", + "enforcement": "report" + }, + "rank": { + "type": "number", + "description": "rank for top/bottom N rules. Aliases: top, bottomN.", + "aliases": ["top", "bottomN"], + "add": true, "set": true, "get": true, + "examples": ["--prop type=topN --prop rank=10"], + "readback": "integer rank", + "enforcement": "report" + }, + "percent": { + "type": "bool", + "description": "treat 'rank' as a percentile rather than count (top/bottom rules).", + "add": true, "set": true, "get": true, + "examples": ["--prop type=top --prop rank=10 --prop percent=true"], + "readback": "true/false", + "enforcement": "report" + }, + "bottom": { + "type": "bool", + "description": "select bottom-N instead of top-N (top/bottom rules).", + "add": true, "set": true, "get": true, + "examples": ["--prop type=bottom --prop rank=5"], + "readback": "true/false", + "enforcement": "report" + }, + "aboveAverage": { + "type": "bool", + "description": "aboveAverage rule: true=above, false=below. Alias: above.", + "aliases": ["above"], + "add": true, "set": false, "get": true, + "examples": ["--prop type=aboveAverage --prop aboveAverage=true"], + "readback": "true/false", + "enforcement": "report" + }, + "stdDev": { + "type": "number", + "description": "stdDev offset for aboveAverage rules (1 = 1σ above mean). Add-time only — Get does not surface this back today.", + "add": true, "set": false, "get": false, + "examples": ["--prop stdDev=1"], + "readback": "n/a", + "enforcement": "report" + }, + "equalAverage": { + "type": "bool", + "description": "include the mean in aboveAverage matching. Add-time only — Get does not surface this back today.", + "add": true, "set": false, "get": false, + "examples": ["--prop equalAverage=true"], + "readback": "n/a", + "enforcement": "report" + }, + "period": { + "type": "string", + "description": "time-period token for dateOccurring rules (today, yesterday, tomorrow, thisWeek, lastWeek, nextWeek, thisMonth, lastMonth, nextMonth). Aliases: timePeriod.", + "aliases": ["timePeriod", "timeperiod"], + "add": true, "set": false, "get": true, + "examples": ["--prop type=dateOccurring --prop period=lastWeek"], + "readback": "period token", + "enforcement": "report" + }, + "min": { + "type": "string", + "description": "data-bar minimum value (numeric or 'auto'). Used by dataBar rules.", + "add": true, "set": false, "get": true, + "examples": ["--prop type=dataBar --prop min=0 --prop max=100"], + "readback": "number or token", + "enforcement": "report" + }, + "max": { + "type": "string", + "description": "data-bar maximum value (numeric or 'auto'). Used by dataBar rules.", + "add": true, "set": false, "get": true, + "examples": ["--prop type=dataBar --prop max=100"], + "readback": "number or token", + "enforcement": "report" + }, + "showValue": { + "type": "bool", + "description": "show numeric label alongside data bar / icon set. Alias: showvalue.", + "aliases": ["showvalue"], + "add": true, "set": true, "get": true, + "examples": ["--prop showValue=false"], + "readback": "true/false", + "enforcement": "report" + }, + "negativeColor": { + "type": "color", + "description": "data-bar fill color for negative values.", + "add": true, "set": false, "get": true, + "examples": ["--prop negativeColor=#FF0000"], + "readback": "#-prefixed hex", + "enforcement": "report" + }, + "axisColor": { + "type": "color", + "description": "data-bar axis color (separator between positive and negative bars).", + "add": true, "set": false, "get": true, + "examples": ["--prop axisColor=#000000"], + "readback": "#-prefixed hex", + "enforcement": "report" + }, + "axisPosition": { + "type": "enum", + "values": ["automatic", "middle", "none"], + "description": "data-bar axis position. Default: automatic. Add-time only — Get does not surface this back today.", + "add": true, "set": false, "get": false, + "examples": ["--prop axisPosition=middle"], + "readback": "n/a", + "enforcement": "report" + }, + "minColor": { + "type": "color", + "description": "color-scale color at the minimum stop. Alias: mincolor.", + "aliases": ["mincolor"], + "add": true, "set": true, "get": true, + "examples": ["--prop type=colorScale --prop minColor=#F8696B"], + "readback": "#-prefixed hex", + "enforcement": "report" + }, + "maxColor": { + "type": "color", + "description": "color-scale color at the maximum stop. Alias: maxcolor.", + "aliases": ["maxcolor"], + "add": true, "set": true, "get": true, + "examples": ["--prop maxColor=#63BE7B"], + "readback": "#-prefixed hex", + "enforcement": "report" + }, + "midColor": { + "type": "color", + "description": "color-scale color at the midpoint stop. Alias: midcolor.", + "aliases": ["midcolor"], + "add": true, "set": false, "get": true, + "examples": ["--prop midColor=#FFEB84"], + "readback": "#-prefixed hex", + "enforcement": "report" + }, + "midPoint": { + "type": "string", + "description": "color-scale midpoint value (numeric or percentile). Alias: midpoint. Add-time only — Get does not surface this back today.", + "aliases": ["midpoint"], + "add": true, "set": false, "get": false, + "examples": ["--prop midPoint=50"], + "readback": "n/a", + "enforcement": "report" + }, + "iconset": { + "type": "string", + "description": "icon-set name (e.g. 3TrafficLights1, 3Arrows, 5Rating). Alias: icons.", + "aliases": ["icons"], + "add": true, "set": true, "get": true, + "examples": ["--prop type=iconSet --prop iconset=3TrafficLights1"], + "readback": "icon-set name", + "enforcement": "report" + }, + "reverse": { + "type": "bool", + "description": "reverse the icon-set ordering.", + "add": true, "set": true, "get": true, + "examples": ["--prop reverse=true"], + "readback": "true/false", + "enforcement": "report" + }, + "formula": { + "type": "string", + "description": "formulaCf expression (without leading '=').", + "add": true, "set": true, "get": true, + "examples": ["--prop type=formula --prop formula=ISERROR(A1)"], + "readback": "formula expression", + "enforcement": "report" + }, + "type": { + "type": "enum", + "values": ["dataBar", "colorScale", "iconSet", "formula", "topN", "aboveAverage", "duplicateValues", "uniqueValues", "containsText", "cellIs", "timePeriod"], + "description": "Canonical Get-only CF rule type token (camelCase). One key per semantic value — previously split as ruleType + cfType.", + "add": false, "set": false, "get": true, + "readback": "canonical CF type token", + "enforcement": "report" + }, + "dxfId": { + "type": "int", + "description": "Get-only readback of the differential format index referenced by this rule (links to the workbook-level dxfs table).", + "add": false, "set": false, "get": true, + "readback": "0-based dxf index", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/containstext.json b/schemas/help/xlsx/containstext.json new file mode 100644 index 000000000..229248851 --- /dev/null +++ b/schemas/help/xlsx/containstext.json @@ -0,0 +1,19 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "containstext", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Match cells whose text contains a substring. Add via `add /Sheet1/cf --type containstext --prop sqref=A1:A100 --prop text=error --prop fill=FFCCCC`. Lookup: Add.Cf.cs:655 (AddCfExtended `containstext` case); Get: Query.cs:577.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A100"], "enforcement":"report" }, + "text": { "type":"string", "add":true, "get":true, "description":"substring to match (case-insensitive). Required.", "examples":["--prop text=error"], "readback":"matched substring", "enforcement":"report" }, + "fill": { "type":"color", "add":true, "get":false, "description":"background fill via dxf.", "examples":["--prop fill=FFCCCC"], "enforcement":"report" }, + "font.color": { "type":"color", "add":true, "get":false, "description":"font color via dxf.", "examples":["--prop font.color=FF0000"], "enforcement":"report" }, + "font.bold": { "type":"bool", "add":true, "get":false, "description":"bold via dxf.", "examples":["--prop font.bold=true"], "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/databar.json b/schemas/help/xlsx/databar.json new file mode 100644 index 000000000..9df7c4970 --- /dev/null +++ b/schemas/help/xlsx/databar.json @@ -0,0 +1,25 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "databar", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Conditional formatting data bar rule. Add via `add /Sheet1/cf --type databar --prop sqref=A1:A10`. Lookup: Add.Cf.cs:84 (AddDataBar); Get readback: Query.cs:464.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A10"], "enforcement":"report" }, + "min": { "type":"string", "add":true, "get":false, "description":"data bar lower bound (omit for auto-min).", "examples":["--prop min=0"], "enforcement":"report" }, + "max": { "type":"string", "add":true, "get":false, "description":"data bar upper bound (omit for auto-max).", "examples":["--prop max=100"], "enforcement":"report" }, + "color": { "type":"color", "add":true, "get":true, "description":"primary bar color (default 638EC6).", "examples":["--prop color=4472C4"], "readback":"#-prefixed uppercase hex or 'themeN'", "enforcement":"report" }, + "showValue": { "type":"bool", "add":true, "get":true, "description":"show cell value alongside the bar (default true).", "examples":["--prop showValue=false"], "readback":"true | false (only emitted when false)", "enforcement":"report" }, + "negativeColor": { "type":"color", "add":true, "get":true, "description":"negative-value bar color (x14 extension, default FF0000).", "examples":["--prop negativeColor=FF0000"], "readback":"#-prefixed uppercase hex", "enforcement":"report" }, + "axisColor": { "type":"color", "add":true, "get":true, "description":"axis color (x14 extension, default 000000).", "examples":["--prop axisColor=000000"], "readback":"#-prefixed uppercase hex", "enforcement":"report" }, + "axisPosition": { "type":"enum", "values":["automatic","middle","none"], "add":true, "get":false, "description":"axis position for negative values (x14 extension, default automatic).", "examples":["--prop axisPosition=middle"], "enforcement":"report" }, + "minLength": { "type":"number", "add":true, "get":true, "description":"minimum bar length (% of cell, default 0).", "examples":["--prop minLength=10"], "readback":"integer percentage", "enforcement":"report" }, + "maxLength": { "type":"number", "add":true, "get":true, "description":"maximum bar length (% of cell, default 100).", "examples":["--prop maxLength=90"], "readback":"integer percentage", "enforcement":"report" }, + "direction": { "type":"enum", "values":["leftToRight","rightToLeft","context","ltr","rtl"], "add":true, "get":true, "description":"bar direction (x14 extension).", "examples":["--prop direction=leftToRight"], "readback":"OOXML direction token", "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/dateoccurring.json b/schemas/help/xlsx/dateoccurring.json new file mode 100644 index 000000000..51ac04087 --- /dev/null +++ b/schemas/help/xlsx/dateoccurring.json @@ -0,0 +1,19 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "dateoccurring", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Match dates falling in a named time period. Aliases for type: timeperiod. Add via `add /Sheet1/cf --type dateoccurring --prop sqref=A1:A100 --prop period=last7Days`. Lookup: Add.Cf.cs:669 (AddCfExtended `dateoccurring` case); Get: Query.cs:597.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A100"], "enforcement":"report" }, + "period": { "type":"enum", "values":["today","yesterday","tomorrow","last7Days","thisWeek","lastWeek","nextWeek","thisMonth","lastMonth","nextMonth"], "aliases":["timePeriod","timeperiod"], "add":true, "get":true, "description":"time period token (default today).", "examples":["--prop period=last7Days","--prop period=today"], "readback":"OOXML time-period token", "enforcement":"report" }, + "fill": { "type":"color", "add":true, "get":false, "description":"background fill via dxf.", "examples":["--prop fill=FFCCCC"], "enforcement":"report" }, + "font.color": { "type":"color", "add":true, "get":false, "description":"font color via dxf.", "examples":["--prop font.color=FF0000"], "enforcement":"report" }, + "font.bold": { "type":"bool", "add":true, "get":false, "description":"bold via dxf.", "examples":["--prop font.bold=true"], "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/duplicatevalues.json b/schemas/help/xlsx/duplicatevalues.json new file mode 100644 index 000000000..c827012f7 --- /dev/null +++ b/schemas/help/xlsx/duplicatevalues.json @@ -0,0 +1,18 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "duplicatevalues", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Highlight duplicate values in range. Add via `add /Sheet1/cf --type duplicatevalues --prop sqref=A1:A100 --prop fill=FFCCCC`. Lookup: Add.Cf.cs:646 (AddCfExtended `duplicatevalues` case); Get: Query.cs:563.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A100"], "enforcement":"report" }, + "fill": { "type":"color", "add":true, "get":false, "description":"background fill via dxf.", "examples":["--prop fill=FFCCCC"], "enforcement":"report" }, + "font.color": { "type":"color", "add":true, "get":false, "description":"font color via dxf.", "examples":["--prop font.color=FF0000"], "enforcement":"report" }, + "font.bold": { "type":"bool", "add":true, "get":false, "description":"bold via dxf.", "examples":["--prop font.bold=true"], "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/formulacf.json b/schemas/help/xlsx/formulacf.json new file mode 100644 index 000000000..6b6f56903 --- /dev/null +++ b/schemas/help/xlsx/formulacf.json @@ -0,0 +1,24 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "formulacf", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Formula-based conditional formatting. Add via `add /Sheet1/cf --type formula --prop sqref=A1:A10 --prop formula=\"$A1>100\" --prop fill=FFFF00`. Lookup: Add.Cf.cs:382 (AddFormulaCf); Get: Query.cs:536.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A10"], "enforcement":"report" }, + "formula": { "type":"string", "add":true, "get":true, "description":"formula expression evaluated per-cell (without leading '='). Required.", "examples":["--prop formula=\"$A1>100\"","--prop formula=\"MOD(ROW(),2)=0\""], "readback":"formula text as stored", "enforcement":"report" }, + "fill": { "type":"color", "add":true, "get":false, "description":"background fill color applied via differential format (dxf).", "examples":["--prop fill=FFFF00"], "enforcement":"report" }, + "font.color": { "type":"color", "add":true, "get":false, "description":"font color via dxf.", "examples":["--prop font.color=FF0000"], "enforcement":"report" }, + "font.bold": { "type":"bool", "add":true, "get":false, "description":"bold via dxf.", "examples":["--prop font.bold=true"], "enforcement":"report" }, + "font.italic": { "type":"bool", "add":true, "get":false, "description":"italic via dxf.", "examples":["--prop font.italic=true"], "enforcement":"report" }, + "font.underline": { "type":"bool", "add":true, "get":false, "description":"underline via dxf.", "examples":["--prop font.underline=true"], "enforcement":"report" }, + "font.strike": { "type":"bool", "add":true, "get":false, "description":"strikethrough via dxf.", "examples":["--prop font.strike=true"], "enforcement":"report" }, + "font.size": { "type":"font-size", "add":true, "get":false, "description":"font size via dxf.", "examples":["--prop font.size=12pt"], "enforcement":"report" }, + "font.name": { "type":"string", "add":true, "get":false, "description":"font family via dxf.", "examples":["--prop font.name=Arial"], "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/hyperlink.json b/schemas/help/xlsx/hyperlink.json new file mode 100644 index 000000000..1ec169088 --- /dev/null +++ b/schemas/help/xlsx/hyperlink.json @@ -0,0 +1,70 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "hyperlink", + "parent": "cell", + "operations": { + "add": false, + "set": false, + "get": true, + "query": true, + "remove": false + }, + "paths": { + "positional": [ + "/SheetName/CellRef" + ] + }, + "note": "In Excel, hyperlinks are a cell-level property, not a standalone addressable element. To create or modify one, use `officecli xlsx add cell` / `set` on the owning cell with `--prop link=URL` (optionally `tooltip=`, `display=`). Query returns discoverable hyperlink nodes whose `Path` points at the owning cell (e.g. `/Sheet1/A1`) so agents can Get/Set from there. Removal: Set the cell's `link=none`. Aliases on cell input: link, href.", + "extends": "_shared/hyperlink", + "properties": { + "url": { + "type": "string", + "description": "external URL readback (read-only at this element). For cell-level Set use cell `link=URL`.", + "add": false, + "set": false, + "get": true, + "readback": "URL string", + "enforcement": "report" + }, + "ref": { + "type": "string", + "description": "target cell range. Readback only (from ).", + "add": false, + "set": false, + "get": true, + "readback": "cell reference", + "enforcement": "report" + }, + "location": { + "type": "string", + "description": "internal sheet/cell target (Sheet1!A1). Readback only here; create via cell `link=` property.", + "add": false, + "set": false, + "get": true, + "readback": "internal target", + "enforcement": "report" + }, + "display": { + "type": "string", + "description": "display text. Readback only here; set via cell `display=` property.", + "add": false, + "set": false, + "get": true, + "readback": "display string", + "enforcement": "report" + }, + "tooltip": { + "type": "string", + "description": "hover tooltip. Readback only here; set via cell `tooltip=` property.", + "add": false, + "set": false, + "get": true, + "readback": "tooltip text", + "enforcement": "report", + "examples": [ + "--prop tooltip=\"Next section\"" + ] + } + } +} diff --git a/schemas/help/xlsx/iconset.json b/schemas/help/xlsx/iconset.json new file mode 100644 index 000000000..279e170ec --- /dev/null +++ b/schemas/help/xlsx/iconset.json @@ -0,0 +1,17 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "iconset", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Conditional formatting iconset rule. Add via `add /Sheet1/cf --type iconset --prop sqref=A1:A10 --prop iconset=3arrows`. Lookup: Add.Cf.cs:322 (AddIconSet); Get: Query.cs:525.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range", "examples":["--prop ref=A1:A10"], "enforcement":"report" }, + "iconset": { "type":"enum", "values":["3arrows","3arrowsGray","3flags","3trafficLights1","3trafficLights2","3signs","3symbols","3symbols2","4arrows","4arrowsGray","4rating","4redToBlack","4trafficLights","5arrows","5arrowsGray","5rating","5quarters"], "aliases":["icons"], "add":true, "get":true, "description":"icon set name", "readback":"icon set token", "examples":["--prop iconset=3arrows"], "enforcement":"report" }, + "reverse": { "type":"bool", "add":true, "get":false, "description":"reverse icon order", "examples":["--prop reverse=true"], "enforcement":"report" }, + "showValue": { "type":"bool", "add":true, "get":false, "description":"show cell value alongside icon (default true)", "examples":["--prop showValue=false"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/namedrange.json b/schemas/help/xlsx/namedrange.json new file mode 100644 index 000000000..61b529f86 --- /dev/null +++ b/schemas/help/xlsx/namedrange.json @@ -0,0 +1,61 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "namedrange", + "parent": "workbook|sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": ["/namedrange[@name=NAME]", "/namedrange[NAME]"], + "positional": ["/namedrange[N]"] + }, + "note": "Aliases: definedname. Name rules from ECMA-376 §18.2.5 — start with letter/underscore/backslash, only letters/digits/underscore/period/backslash, must not look like a cell ref. refersTo content must not start with '='.", + "properties": { + "name": { + "type": "string", + "description": "defined-name identifier. Required (or inferred from path).", + "add": true, "set": true, "get": true, + "examples": ["--prop name=Revenue"], + "readback": "name", + "enforcement": "report" + }, + "ref": { + "type": "string", + "description": "refersTo expression. Aliases: refersTo, formula. Do not include leading '='.", + "aliases": ["refersTo", "formula"], + "add": true, "set": true, "get": true, + "examples": ["--prop ref=Sheet1!$A$1:$C$10"], + "readback": "refersTo content", + "enforcement": "report" + }, + "scope": { + "type": "string", + "description": "sheet name for local scope, or 'workbook' (default).", + "add": true, "set": true, "get": true, + "examples": ["--prop scope=workbook"], + "readback": "scope descriptor", + "enforcement": "report" + }, + "comment": { + "type": "string", + "description": "free-text description shown in Excel's Name Manager.", + "add": true, "set": true, "get": true, + "examples": ["--prop comment=\"Q4 totals\""], + "readback": "comment text", + "enforcement": "report" + }, + "volatile": { + "type": "bool", + "description": "force recalculation of the defined name on every workbook change (Excel volatile flag).", + "add": true, "set": true, "get": true, + "examples": ["--prop volatile=true"], + "readback": "volatile flag", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/ole.json b/schemas/help/xlsx/ole.json new file mode 100644 index 000000000..68fcbcb8f --- /dev/null +++ b/schemas/help/xlsx/ole.json @@ -0,0 +1,48 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "ole", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/SheetName/ole[N]" + ] + }, + "note": "Aliases: oleobject, object, embed. Binary package + preview image. Anchor accepts cell range (B2:F7) or x/y/width/height in cell units.", + "extends": [ + "_shared/ole", + "_shared/ole.pptx-xlsx" + ], + "properties": { + "anchor": { + "type": "string", + "aliases": [ + "ref" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop anchor=B2:F7" + ], + "readback": "anchor descriptor", + "enforcement": "report" + }, + "shapeId": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "VML shape id of the OLE container (xlsx legacy drawing).", + "readback": "integer shape id", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/pagebreak.json b/schemas/help/xlsx/pagebreak.json new file mode 100644 index 000000000..ded0827ab --- /dev/null +++ b/schemas/help/xlsx/pagebreak.json @@ -0,0 +1,36 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "pagebreak", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/rowbreak[N]", "/SheetName/colbreak[N]"] + }, + "note": "Dispatcher: routes to rowbreak or colbreak based on which of 'col'/'column' or 'row' is supplied. See xlsx/rowbreak.json and xlsx/colbreak.json for the resolved surfaces.", + "properties": { + "row": { + "type": "int", + "description": "row index — routes to rowbreak. Add-time only — Set is not supported (re-add to relocate). Get does not surface this back today; use sheet.rowBreaks for the indexed list.", + "add": true, "set": false, "get": false, + "examples": ["--prop row=20"], + "readback": "n/a (see sheet.rowBreaks)", + "enforcement": "report" + }, + "col": { + "type": "string", + "description": "column index/letter — routes to colbreak. Alias: column. Add-time only — Set is not supported (re-add to relocate). Get does not surface this back today; use sheet.colBreaks for the indexed list.", + "aliases": ["column"], + "add": true, "set": false, "get": false, + "examples": ["--prop col=F"], + "readback": "n/a (see sheet.colBreaks)", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/picture.json b/schemas/help/xlsx/picture.json new file mode 100644 index 000000000..aa7811e93 --- /dev/null +++ b/schemas/help/xlsx/picture.json @@ -0,0 +1,318 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "picture", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/SheetName/picture[N]" + ] + }, + "note": "Aliases: image, img. Anchor via ParseAnchorBoundsEmu — accepts cell counts or unit-qualified lengths. SVG sources get a PNG fallback.", + "extends": [ + "_shared/picture", + "_shared/picture.docx-xlsx", + "_shared/picture.pptx-xlsx" + ], + "properties": { + "crop": { + "type": "string", + "description": "Crop in percent (0-100). 1 value = symmetric, 2 values = vertical,horizontal, 4 values = left,top,right,bottom. Excel reads but does not write crops — overrides shared base which marks add/set true (pptx-only writability).", + "add": false, + "set": false, + "get": true, + "examples": [ + "--prop crop=10" + ], + "readback": "comma-separated percentages (left,top,right,bottom)", + "enforcement": "report" + }, + "title": { + "type": "string", + "description": "OOXML @title attribute on cNvPr (distinct from alt).", + "add": true, + "set": false, + "examples": [ + "--prop title=\"Logo\"" + ], + "enforcement": "report" + }, + "decorative": { + "type": "bool", + "description": "Mark the picture as decorative (a16:decorative ext under cNvPr). Excludes it from screen-reader alt-text traversal.", + "add": true, + "set": false, + "examples": [ + "--prop decorative=true" + ], + "enforcement": "report" + }, + "rotation": { + "type": "string", + "description": "Rotation in degrees (positive = clockwise). Stored OOXML-internal as 60000ths of a degree on Transform2D @rot.", + "add": true, + "set": true, + "examples": [ + "--prop rotation=45" + ], + "enforcement": "report" + }, + "flip": { + "type": "string", + "description": "Compact flip token: 'h' / 'v' / 'both' / 'hv' / 'vh' / 'horizontal' / 'vertical'.", + "add": true, + "set": true, + "examples": [ + "--prop flip=h", + "--prop flip=both" + ], + "enforcement": "report" + }, + "flipH": { + "type": "bool", + "description": "Flip horizontally (Office-API-style alias of flip=h).", + "add": true, + "set": true, + "examples": [ + "--prop flipH=true" + ], + "enforcement": "report" + }, + "flipV": { + "type": "bool", + "description": "Flip vertically (Office-API-style alias of flip=v).", + "add": true, + "set": true, + "examples": [ + "--prop flipV=true" + ], + "enforcement": "report" + }, + "flipBoth": { + "type": "bool", + "description": "Flip both axes (alias of flip=both).", + "add": true, + "set": false, + "examples": [ + "--prop flipBoth=true" + ], + "enforcement": "report" + }, + "opacity": { + "type": "string", + "description": "Picture opacity. Accepts percent (50, '50%') or fraction (0.5). 100 / 100% / 1.0 = fully opaque.", + "add": true, + "set": false, + "examples": [ + "--prop opacity=50", + "--prop opacity=0.5" + ], + "enforcement": "report" + }, + "hyperlink": { + "type": "string", + "description": "Picture-level hyperlink. External URL (https://...) or in-document jump (#SheetName!A1).", + "aliases": [ + "link" + ], + "add": true, + "set": false, + "examples": [ + "--prop hyperlink=https://example.com" + ], + "enforcement": "report" + }, + "anchor": { + "type": "string", + "description": "Cell-range anchor (e.g. 'B2:E6') or anchorMode token ('oneCell'/'twoCell'/'absolute'). Cell-range form implies twoCell mode.", + "add": true, + "set": false, + "examples": [ + "--prop anchor=B2:E6", + "--prop anchor=oneCell" + ], + "enforcement": "report" + }, + "anchorMode": { + "type": "string", + "description": "Explicit anchor mode: 'oneCell' / 'twoCell' / 'absolute'. Overrides any anchor= mode token.", + "add": true, + "set": false, + "examples": [ + "--prop anchorMode=oneCell" + ], + "enforcement": "report" + }, + "shadow": { + "type": "string", + "description": "Outer shadow effect. 'none' to clear, or a color/spec accepted by DrawingEffectsHelper.", + "add": false, + "set": true, + "examples": [ + "--prop shadow=#808080" + ], + "enforcement": "report" + }, + "glow": { + "type": "string", + "description": "Glow effect color/spec. 'none' to clear.", + "add": false, + "set": true, + "examples": [ + "--prop glow=#4472C4" + ], + "enforcement": "report" + }, + "reflection": { + "type": "string", + "description": "Reflection effect. 'none' to clear.", + "add": false, + "set": true, + "examples": [ + "--prop reflection=true" + ], + "enforcement": "report" + }, + "softEdge": { + "type": "string", + "aliases": [ + "softedge" + ], + "description": "Soft edge effect radius. 'none' to clear.", + "add": false, + "set": true, + "examples": [ + "--prop softEdge=5" + ], + "enforcement": "report" + }, + "crop.l": { + "type": "string", + "description": "Crop from left edge as a percentage (e.g. 10 = 10%, '10%' also accepted). Internally stored in 1/1000 percent units.", + "add": true, + "set": true, + "examples": [ + "--prop crop.l=10", + "--prop crop.l=50%" + ], + "enforcement": "report" + }, + "crop.r": { + "type": "string", + "description": "Crop from right edge as a percentage.", + "add": true, + "set": true, + "examples": [ + "--prop crop.r=10" + ], + "enforcement": "report" + }, + "crop.t": { + "type": "string", + "description": "Crop from top edge as a percentage.", + "add": true, + "set": true, + "examples": [ + "--prop crop.t=10" + ], + "enforcement": "report" + }, + "crop.b": { + "type": "string", + "description": "Crop from bottom edge as a percentage.", + "add": true, + "set": true, + "examples": [ + "--prop crop.b=10" + ], + "enforcement": "report" + }, + "srcRect": { + "type": "string", + "description": "Compound crop spec, e.g. 'l=10,r=10,t=5,b=5'. Equivalent to crop.l/crop.r/crop.t/crop.b.", + "add": true, + "set": true, + "examples": [ + "--prop srcRect=l=10,r=10,t=5,b=5" + ], + "enforcement": "report" + }, + "anchoredTo": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "anchor descriptor — sheet/cell-range path the picture is anchored to.", + "readback": "`/SheetName/A1[:Z9]` anchor path", + "enforcement": "report" + }, + "mergeAnchor": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "true when the picture is anchored to a merged-cell region.", + "readback": "true|false", + "enforcement": "report" + }, + "x": { + "type": "length", + "description": "x as TwoCellAnchor column/row index (xlsx cell-anchor positioning, integer).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop x=0", + "--prop x=1in" + ], + "readback": "integer column/row index", + "enforcement": "report" + }, + "y": { + "type": "length", + "description": "y as TwoCellAnchor column/row index (xlsx cell-anchor positioning, integer).", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop y=0", + "--prop y=1in" + ], + "readback": "integer column/row index", + "enforcement": "report" + }, + "width": { + "type": "integer", + "description": "width — TwoCellAnchor column/row span (xlsx cell-anchor positioning, integer)", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop width=5", + "--prop width=3in" + ], + "readback": "integer column/row span", + "enforcement": "report" + }, + "height": { + "type": "integer", + "description": "height — TwoCellAnchor column/row span (xlsx cell-anchor positioning, integer)", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop height=5", + "--prop height=2in" + ], + "readback": "integer column/row span", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/pivottable.json b/schemas/help/xlsx/pivottable.json new file mode 100644 index 000000000..8c732c559 --- /dev/null +++ b/schemas/help/xlsx/pivottable.json @@ -0,0 +1,339 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "pivottable", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/pivottable[N]"] + }, + "note": "Aliases: pivot. 'source' required (e.g. Sheet1!A1:D100). External workbook refs rejected. Position auto-placed after source if omitted. Field axes (rows/cols/filters/values) take comma-separated field names; values use 'Field:agg' tuples. Query returns child nodes typed pivotfield/pivotrow/pivotcolumn/pivotdata — these are read-only structural nodes, not independently addressable elements; no Add/Set/Remove is supported on them.", + "properties": { + "source": { + "type": "string", + "description": "source range. Alias: src. External workbook refs rejected.", + "aliases": ["src"], + "add": true, "set": true, "get": true, + "examples": ["--prop source=Sheet1!A1:D100"], + "readback": "source reference", + "enforcement": "report" + }, + "position": { + "type": "string", + "description": "anchor cell (e.g. H1). Alias: pos. Auto-placed after source if omitted.", + "aliases": ["pos"], + "add": true, "set": false, "get": false, + "examples": ["--prop position=H1"], + "readback": "anchor cell", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "pivot table name (identifier).", + "add": true, "set": true, "get": true, + "examples": ["--prop name=RevenueByRegion"], + "readback": "pivot name", + "enforcement": "report" + }, + "style": { + "type": "string", + "description": "built-in or workbook custom pivot style name (e.g. PivotStyleMedium9).", + "add": true, "set": true, "get": true, + "examples": ["--prop style=PivotStyleMedium9"], + "readback": "style name", + "enforcement": "report" + }, + "rows": { + "type": "string", + "description": "row-axis field names, comma-separated (e.g. 'Region,Category'). Aliases: row, rowField, rowFields.", + "aliases": ["row", "rowField", "rowFields"], + "add": true, "set": true, "get": true, + "examples": ["--prop rows=Region,Category"], + "readback": "comma-separated field names", + "enforcement": "report" + }, + "cols": { + "type": "string", + "description": "column-axis field names, comma-separated. Aliases: col, column, columns, colField, colFields, columnField, columnFields.", + "aliases": ["col", "column", "columns", "colField", "colFields", "columnField", "columnFields"], + "add": true, "set": true, "get": true, + "examples": ["--prop cols=Date"], + "readback": "comma-separated field names", + "enforcement": "report" + }, + "filters": { + "type": "string", + "description": "page/filter-axis field names, comma-separated. Aliases: filter, filterField, filterFields.", + "aliases": ["filter", "filterField", "filterFields"], + "add": true, "set": true, "get": true, + "examples": ["--prop filters=Year"], + "readback": "comma-separated field names", + "enforcement": "report" + }, + "values": { + "type": "string", + "description": "value-axis fields as 'Field:agg' tuples, comma-separated (e.g. 'Sales:sum,Qty:avg'). agg one of sum, avg, count, max, min, product, stdev, stdevp, var, varp, countNums. Aliases: value, valueField, valueFields.", + "aliases": ["value", "valueField", "valueFields"], + "add": true, "set": true, "get": true, + "examples": ["--prop values=Sales:sum,Qty:avg"], + "readback": "value field tuples", + "enforcement": "report" + }, + "aggregate": { + "type": "string", + "description": "default aggregate for value fields when omitted from --prop values. One of sum, avg, count, max, min, product, stdev, stdevp, var, varp, countNums.", + "add": true, "set": true, "get": false, + "examples": ["--prop aggregate=avg"], + "readback": "n/a (per-value override)", + "enforcement": "report" + }, + "showDataAs": { + "type": "string", + "description": "value-field display mode: normal, percentOfTotal, percentOfRow, percentOfCol, runningTotal. (percentOfParent / rankAscending / rankDescending / index / difference / percentDifference are not yet supported.)", + "aliases": ["showdataas"], + "add": true, "set": true, "get": false, + "examples": ["--prop showDataAs=percentOfTotal"], + "readback": "n/a", + "enforcement": "report" + }, + "topN": { + "type": "string", + "description": "keep only top-N row keys ranked by first value field's aggregate. Integer >= 1; filter applied to source rows pre-cache. Add-time only — Set ignores this key.", + "aliases": ["topn"], + "add": true, "set": false, "get": false, + "examples": ["--prop topN=10"], + "readback": "n/a (filters source rows)", + "enforcement": "report" + }, + "sort": { + "type": "enum", + "values": ["asc", "desc", "locale", "locale-desc", "none"], + "description": "axis-label sort. 'none' (or empty) clears sort.", + "add": true, "set": true, "get": false, + "examples": ["--prop sort=desc"], + "readback": "n/a (label order in axis)", + "enforcement": "report" + }, + "layout": { + "type": "enum", + "values": ["compact", "outline", "tabular"], + "description": "report layout mode. Default: compact.", + "add": true, "set": true, "get": true, + "examples": ["--prop layout=tabular"], + "readback": "layout name", + "enforcement": "report" + }, + "labelFilter": { + "type": "string", + "description": "row-level pre-cache label filter as 'field:type:value' (e.g. 'Region:beginsWith:N'). Type one of equals, notEquals, beginsWith, endsWith, contains, notContains, greaterThan, greaterThanEqual, lessThan, lessThanEqual, between, notBetween. Add-time only — Set ignores this key.", + "aliases": ["labelfilter"], + "add": true, "set": false, "get": false, + "examples": ["--prop labelFilter=Region:beginsWith:N"], + "readback": "n/a (filters source rows)", + "enforcement": "report" + }, + "calculatedField": { + "type": "string", + "description": "user-defined formula field as 'Name:=Formula' (e.g. 'Margin:=Sales-Cost'). Use calculatedField1, calculatedField2, ... for multiple. Alias: calculatedFields. Add-time only — Set ignores this key.", + "aliases": ["calculatedfield", "calculatedfields"], + "add": true, "set": false, "get": false, + "examples": ["--prop calculatedField=Margin:=Sales-Cost", "--prop calculatedField1=Margin:=Sales-Cost --prop calculatedField2=Tax:=Sales*0.1"], + "readback": "n/a", + "enforcement": "report" + }, + "repeatLabels": { + "type": "bool", + "description": "repeat outer-axis item labels on every row (fillDown). Aliases: repeatItemLabels, repeatAllLabels, fillDownLabels.", + "aliases": ["repeatlabels", "repeatItemLabels", "repeatAllLabels", "fillDownLabels"], + "add": true, "set": true, "get": true, + "examples": ["--prop repeatLabels=true"], + "readback": "true/false", + "enforcement": "report" + }, + "blankRows": { + "type": "bool", + "description": "insert a blank row after each outer item group. Aliases: insertBlankRow, insertBlankRows, blankRow, blankLine, blankLines.", + "aliases": ["blankrows", "insertBlankRow", "insertBlankRows", "blankRow", "blankLine", "blankLines"], + "add": true, "set": true, "get": true, + "examples": ["--prop blankRows=true"], + "readback": "true/false", + "enforcement": "report" + }, + "grandTotals": { + "type": "enum", + "values": ["both", "none", "rows", "cols", "on", "off", "true", "false"], + "description": "grand-total visibility. 'rows' = show grand-total row only; 'cols' = show grand-total column only; 'both'/'on'/'true' = both; 'none'/'off'/'false' = neither.", + "aliases": ["grandtotals"], + "add": true, "set": true, "get": false, + "examples": ["--prop grandTotals=both"], + "readback": "n/a (use rowGrandTotals/colGrandTotals on get)", + "enforcement": "report" + }, + "rowGrandTotals": { + "type": "bool", + "description": "show row-axis grand totals. Independent of colGrandTotals.", + "aliases": ["rowgrandtotals"], + "add": true, "set": true, "get": true, + "examples": ["--prop rowGrandTotals=true"], + "readback": "true/false", + "enforcement": "report" + }, + "colGrandTotals": { + "type": "bool", + "description": "show column-axis grand totals. Alias: columnGrandTotals.", + "aliases": ["colgrandtotals", "columnGrandTotals"], + "add": true, "set": true, "get": true, + "examples": ["--prop colGrandTotals=true"], + "readback": "true/false", + "enforcement": "report" + }, + "grandTotalCaption": { + "type": "string", + "description": "label text for the Grand Total row/column (default 'Grand Total').", + "aliases": ["grandtotalcaption"], + "add": true, "set": true, "get": true, + "examples": ["--prop grandTotalCaption=\"Total\""], + "readback": "caption text", + "enforcement": "report" + }, + "subtotals": { + "type": "enum", + "values": ["on", "off", "true", "false", "show", "hide", "yes", "no", "1", "0"], + "description": "show/hide outer-level subtotal rows.", + "add": true, "set": true, "get": true, + "examples": ["--prop subtotals=off"], + "readback": "on/off", + "enforcement": "report" + }, + "defaultSubtotal": { + "type": "bool", + "description": "default-subtotal flag for new pivot fields (per-field ).", + "aliases": ["defaultsubtotal"], + "add": true, "set": true, "get": false, + "examples": ["--prop defaultSubtotal=true"], + "readback": "n/a (per-field)", + "enforcement": "report" + }, + "showRowStripes": { + "type": "bool", + "description": "banded-row striping in the pivot style. Alias: bandedRows.", + "aliases": ["showrowstripes", "bandedRows"], + "add": true, "set": true, "get": true, + "examples": ["--prop showRowStripes=true"], + "readback": "true/false", + "enforcement": "report" + }, + "showColStripes": { + "type": "bool", + "description": "banded-column striping in the pivot style. Aliases: showColumnStripes, bandedCols, bandedColumns.", + "aliases": ["showcolstripes", "showColumnStripes", "bandedCols", "bandedColumns"], + "add": true, "set": true, "get": true, + "examples": ["--prop showColStripes=true"], + "readback": "true/false", + "enforcement": "report" + }, + "showRowHeaders": { + "type": "bool", + "description": "show row-axis header formatting (pivotTableStyleInfo).", + "aliases": ["showrowheaders"], + "add": true, "set": true, "get": true, + "examples": ["--prop showRowHeaders=true"], + "readback": "true/false", + "enforcement": "report" + }, + "showColHeaders": { + "type": "bool", + "description": "show column-axis header formatting. Alias: showColumnHeaders.", + "aliases": ["showcolheaders", "showColumnHeaders"], + "add": true, "set": true, "get": true, + "examples": ["--prop showColHeaders=true"], + "readback": "true/false", + "enforcement": "report" + }, + "showLastColumn": { + "type": "bool", + "description": "highlight the last column in the pivot style.", + "aliases": ["showlastcolumn"], + "add": true, "set": true, "get": true, + "examples": ["--prop showLastColumn=true"], + "readback": "true/false", + "enforcement": "report" + }, + "showDrill": { + "type": "bool", + "description": "show expand/collapse (+/-) buttons on every pivot field. Add-time only — Set ignores this key.", + "aliases": ["showdrill"], + "add": true, "set": false, "get": false, + "examples": ["--prop showDrill=false"], + "readback": "n/a", + "enforcement": "report" + }, + "mergeLabels": { + "type": "bool", + "description": "merge+center repeated outer-axis item cells (). Add-time only — Set ignores this key.", + "aliases": ["mergelabels"], + "add": true, "set": false, "get": false, + "examples": ["--prop mergeLabels=true"], + "readback": "n/a", + "enforcement": "report" + }, + "cacheId": { + "type": "number", + "description": "pivot cache index (read-only; assigned by Excel when the pivot table is created).", + "add": false, "set": false, "get": true, + "readback": "pivot cache index (read-only; assigned by Excel)", + "enforcement": "report" + }, + "fieldCount": { + "type": "number", + "description": "total number of pivot fields in the source range (read-only).", + "add": false, "set": false, "get": true, + "readback": "total number of pivot fields in the source range", + "enforcement": "report" + }, + "dataFieldCount": { + "type": "number", + "description": "number of data field aggregations (read-only; equals the count of dataField{N} entries).", + "add": false, "set": false, "get": true, + "readback": "number of data field aggregations", + "enforcement": "report" + }, + "dataField{N}": { + "type": "string", + "description": "per-data-field readback (1-indexed: dataField1, dataField2, …) packed as 'name:aggFunc:fieldIdx'. Get also returns `dataField{N}.showAs` when ShowDataAs != normal.", + "add": false, "set": false, "get": true, + "readback": "packed as 'name:aggFunc:fieldIdx'; name reflects Excel's stored DataField/@name which includes auto-prefixes (e.g. 'Sum of Revenue:sum:1')", + "enforcement": "report" + }, + "dataField{N}.showAs": { + "type": "enum", + "values": ["percent_of_total", "percent_of_row", "percent_of_col", "running_total", "difference", "percent_diff", "index"], + "description": "data field showAs token (read-only). Values are canonicalized from OOXML ShowDataAs.", + "add": false, "set": false, "get": true, + "readback": "showAs canonical token", + "enforcement": "report" + }, + "location": { + "type": "string", + "add": false, "set": false, "get": true, + "description": "target cell range where the pivot table is rendered (e.g. D1:E4).", + "readback": "A1 range string", + "enforcement": "report" + }, + "collapsedFields": { + "type": "string", + "add": false, "set": false, "get": true, + "description": "comma-separated names of fields with collapsed items.", + "readback": "comma-separated field names", + "enforcement": "report" + }, + "axisAsDataField": { "type":"bool", "add":false, "set":false, "get":true, "description":"comma-separated names of fields acting as data field on axis.", "readback":"comma-separated field names", "enforcement":"report" }, + "sortByField": { "type":"string", "add":false, "set":false, "get":true, "description":"comma-separated 'field:direction' sort tuples.", "readback":"comma-separated sort tuples", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/range.json b/schemas/help/xlsx/range.json new file mode 100644 index 000000000..91cd95004 --- /dev/null +++ b/schemas/help/xlsx/range.json @@ -0,0 +1,65 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "range", + "parent": "sheet", + "container": true, + "operations": { + "add": false, + "set": true, + "get": true, + "query": true, + "remove": false + }, + "paths": { + "positional": ["/SheetName/A1:C10"] + }, + "note": "Read-through container for cell ranges. Get returns a range node with cell list / aggregate preview. Set broadcasts formatting props to every cell in the range (font/color/numberformat/alignment/fill). Not an Add target — cells exist implicitly.", + "properties": { + "merge": { + "type": "bool", + "description": "merge all cells in the range into a single cell. Pass merge=true to merge; the range is taken from the path. A range-shaped value equal to the path's range (e.g. `set '/Sheet1/B2:C2' --prop merge=B2:C2`) is accepted as a convenience and is equivalent to merge=true; a range-shaped value that disagrees with the path is rejected. Set merge=false to unmerge — the range must exactly match an existing merge, otherwise the call fails with the precise refs to use. Pass merge=sweep to bulk-clear every merge contained in the range (destructive, no precision check). For multiple disjoint ranges in one call, use the prop value form on a sheet- or cell-anchored set (e.g. `set '/Sheet1' --prop merge=A1:B1,A2:B2`).", + "add": false, "set": true, "get": true, + "examples": ["--prop merge=true", "--prop merge=false", "--prop merge=sweep"], + "readback": "true/false", + "enforcement": "report" + }, + "font.bold": { + "type": "bool", + "description": "broadcast bold to every cell.", + "add": false, "set": true, "get": false, + "examples": ["--prop font.bold=true"], + "readback": "n/a (broadcast)", + "enforcement": "report" + }, + "fill": { + "type": "color", + "description": "broadcast fill color.", + "add": false, "set": true, "get": false, + "examples": ["--prop fill=#FFFF00"], + "readback": "n/a (broadcast)", + "enforcement": "report" + }, + "numberformat": { + "type": "string", + "description": "broadcast number format code.", + "aliases": ["format"], + "add": false, "set": true, "get": false, + "examples": ["--prop numberformat=\"#,##0.00\""], + "readback": "n/a (broadcast)", + "enforcement": "report" + }, + "alignment.horizontal": { + "type": "enum", + "values": ["left", "center", "right", "justify", "general", "fill", "centerContinuous"], + "aliases": ["halign"], + "add": false, "set": true, "get": false, + "examples": ["--prop alignment.horizontal=center"], + "readback": "n/a (broadcast)", + "enforcement": "report" + } + }, + "children": [ + { "element": "cell", "pathSegment": "{CellRef}", "cardinality": "1..n" } + ] +} diff --git a/schemas/help/xlsx/raw.json b/schemas/help/xlsx/raw.json new file mode 100644 index 000000000..6c0a58619 --- /dev/null +++ b/schemas/help/xlsx/raw.json @@ -0,0 +1,33 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "raw", + "operations": { + "add": false, + "set": false, + "get": false, + "query": false, + "remove": false + }, + "properties": {}, + "description": "Raw OOXML access via the `raw` (read) and `raw-set` (write) commands. Use only when L2 DOM operations cannot express what you need. Two part-path forms are accepted: (1) semantic short names (recommended — stable across sheet rename/reorder) and (2) zip-internal URIs (any path ending in .xml is resolved literally against the package, e.g. /xl/worksheets/sheet1.xml).", + "parts": [ + { "name": "/workbook", "desc": "Workbook part (sheet refs, defined names, calc properties)" }, + { "name": "/styles", "desc": "Stylesheet (fonts, fills, borders, numFmts, cellXfs)" }, + { "name": "/sharedStrings", "desc": "Shared string table" }, + { "name": "/theme", "desc": "Theme (color scheme, font scheme)" }, + { "name": "/", "desc": "Worksheet by name, e.g. /Sheet1" }, + { "name": "//drawing", "desc": "Drawing part for that sheet (shapes, charts, pictures)" }, + { "name": "//chart[N]", "desc": "Nth chart on the named sheet" }, + { "name": "/chart[N]", "desc": "Nth chart globally (across all sheets)" }, + { "name": "//", "desc": "Sheet-relationship part by relId (covers OLE embeds, images, etc.)" }, + { "name": "/.xml", "desc": "Any path ending in .xml is resolved as a literal zip-internal URI (e.g. /xl/worksheets/sheet1.xml, /xl/styles.xml, /xl/sharedStrings.xml, /xl/theme/theme1.xml). Use when you need positional-by-zip-order access; semantic names are preferred for stability." } + ], + "examples": [ + "officecli raw data.xlsx /workbook", + "officecli raw data.xlsx /Sheet1", + "officecli raw data.xlsx /styles", + "officecli raw data.xlsx /xl/worksheets/sheet1.xml", + "officecli raw-set data.xlsx /Sheet1 --xpath \"//x:c[@r='A1']\" --action replace --xml \"42\"" + ] +} diff --git a/schemas/help/xlsx/row.json b/schemas/help/xlsx/row.json new file mode 100644 index 000000000..dce6e2d8e --- /dev/null +++ b/schemas/help/xlsx/row.json @@ -0,0 +1,68 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "row", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/row[N]"] + }, + "note": "Row index N is 1-based. Add/Set asymmetry: all formatting props are currently Set-only. Insert/move/clone at an occupied slot shifts every row at or past the slot down by one, and every range-bearing structure on the sheet (mergeCells, CF/DV sqref, autoFilter, hyperlink/table refs, named ranges, formula cell-refs) follows the displacement. 'add --from /SheetName/row[K]' with --before/--after clones cells + single-row mergeCells; relative formula refs are delta-shifted to the new anchor (Excel 'Insert Copied Cells' parity).", + "properties": { + "cols": { + "type": "int", + "description": "number of empty cells to seed in the new row.", + "add": true, "set": false, "get": false, + "examples": ["--prop cols=5"], + "readback": "n/a (structural only)", + "enforcement": "strict" + }, + "height": { + "type": "length", + "description": "row height in points.", + "add": true, "set": true, "get": true, + "examples": ["--prop height=24"], + "readback": "numeric points (raw double as stored)", + "enforcement": "strict" + }, + "hidden": { + "type": "bool", + "description": "hide row.", + "add": true, "set": true, "get": true, + "examples": ["--prop hidden=true"], + "readback": "true when hidden, key absent otherwise", + "enforcement": "strict" + }, + "outline": { + "type": "int", + "description": "outline/group level 0-7. Aliases: outlineLevel, group. Set accepts `outline`/`outlineLevel`/`group`; Get readback uses canonical key `outlineLevel`.", + "aliases": ["outlinelevel", "group"], + "add": false, "set": true, "get": false, + "examples": ["--prop outline=1"], + "readback": "see `outlineLevel`", + "enforcement": "report" + }, + "outlineLevel": { + "type": "number", + "description": "row outline grouping level (0 = ungrouped, 1..7 = nested group depth). Get-only readback of the value set via `outline`.", + "add": false, "set": false, "get": true, + "readback": "integer 0..7; key omitted when row has no outline level", + "enforcement": "report" + }, + "collapsed": { + "type": "bool", + "description": "collapse row group. Currently Set-only.", + "add": false, "set": true, "get": false, + "examples": ["--prop collapsed=true"], + "readback": "not surfaced by Get", + "enforcement": "report" + }, + "customHeight": { "type":"bool", "add":false, "set":false, "get":true, "description":"true when the row carries an explicit height (Row @customHeight). Get-only flag.", "readback":"true when row has a custom height", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/rowbreak.json b/schemas/help/xlsx/rowbreak.json new file mode 100644 index 000000000..0793c43ca --- /dev/null +++ b/schemas/help/xlsx/rowbreak.json @@ -0,0 +1,28 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "rowbreak", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/rowbreak[N]"] + }, + "note": "Manual page break before the specified row. 'pagebreak' with col= routes to colbreak.", + "properties": { + "row": { + "type": "int", + "description": "1-based row index where the break occurs. Alias: index.", + "aliases": ["index"], + "add": true, "set": false, "get": false, + "examples": ["--prop row=20"], + "readback": "n/a (see sheet.rowBreaks)", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/run.json b/schemas/help/xlsx/run.json new file mode 100644 index 000000000..ea0cf70e4 --- /dev/null +++ b/schemas/help/xlsx/run.json @@ -0,0 +1,23 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "run", + "parent": "cell", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/SheetName/CellRef/r[N]" + ] + }, + "note": "Rich-text run inside an inline-string cell. Adds an rPh/r element with font properties on the run.", + "extends": [ + "_shared/run", + "_shared/run.docx-xlsx" + ] +} diff --git a/schemas/help/xlsx/shape.json b/schemas/help/xlsx/shape.json new file mode 100644 index 000000000..81e6f5706 --- /dev/null +++ b/schemas/help/xlsx/shape.json @@ -0,0 +1,150 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "shape", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/SheetName/shape[N]" + ] + }, + "note": "Aliases: textbox. Anchor: 'anchor=B2:F7' (cell range) or x/y/width/height in cell units. 'ref=B2' expands to a 1x1 cell rectangle. Font/text props accept either bare ('size', 'bold', 'color', 'font') or dotted ('font.size', 'font.bold', 'font.color', 'font.name') forms.", + "extends": "_shared/shape", + "properties": { + "anchor": { + "type": "string", + "description": "cell range anchor (e.g. B2:F7) — Add-only. Set uses x/y/width/height; Get readback emits x/y/width/height instead of cell-range form (round-trip via numeric position, not anchor string).", + "aliases": [ + "ref" + ], + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop anchor=B2:F7" + ], + "readback": "x/y/width/height (numeric)", + "enforcement": "report" + }, + "gradientFill": { + "type": "string", + "description": "Two/three-stop linear gradient, e.g. 'C1-C2[-C3][:angle]'. Mutually exclusive with fill (gradientFill wins).", + "add": true, + "set": false, + "examples": [ + "--prop gradientFill=FF0000-0000FF:90" + ], + "enforcement": "report" + }, + "geometry": { + "type": "string", + "description": "geometry preset name (rect, ellipse, roundRect, triangle, rightArrow, etc.). Unknown presets fall back to rect with a stderr warning. Set replaces the existing PresetGeometry preserving fill/line/effects.", + "aliases": [ + "preset", + "shape" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop geometry=ellipse" + ], + "enforcement": "report" + }, + "flip": { + "type": "string", + "description": "Compact flip token: 'h' / 'v' / 'both' / 'hv' / 'vh' / 'none'.", + "add": true, + "set": true, + "examples": [ + "--prop flip=h", + "--prop flip=both" + ], + "enforcement": "report" + }, + "flipBoth": { + "type": "bool", + "description": "Flip both axes.", + "add": true, + "set": true, + "examples": [ + "--prop flipBoth=true" + ], + "enforcement": "report" + }, + "x": { + "type": "length", + "description": "x as TwoCellAnchor column/row index. xlsx cell-anchor positioning, integer.", + "aliases": [ + "left" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop x=2", + "--prop x=2cm", + "--prop x=1in", + "--prop x=72pt" + ], + "readback": "integer column/row index", + "enforcement": "strict" + }, + "y": { + "type": "string", + "description": "y as TwoCellAnchor column/row index. xlsx cell-anchor positioning, integer.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop y=3", + "--prop y=3cm" + ], + "enforcement": "report", + "aliases": [ + "top" + ], + "readback": "integer column/row index" + }, + "width": { + "type": "string", + "description": "width as TwoCellAnchor column/row index. xlsx cell-anchor positioning, integer.", + "add": true, + "set": true, + "get": true, + "aliases": [ + "w" + ], + "examples": [ + "--prop width=4", + "--prop width=6cm", + "--prop width=5cm" + ], + "enforcement": "report", + "readback": "integer column/row index" + }, + "height": { + "type": "string", + "description": "height as TwoCellAnchor column/row index. xlsx cell-anchor positioning, integer.", + "add": true, + "set": true, + "get": true, + "aliases": [ + "h" + ], + "examples": [ + "--prop height=3", + "--prop height=3cm" + ], + "enforcement": "report", + "readback": "integer column/row index" + } + } +} diff --git a/schemas/help/xlsx/sheet.json b/schemas/help/xlsx/sheet.json new file mode 100644 index 000000000..ed7a612c2 --- /dev/null +++ b/schemas/help/xlsx/sheet.json @@ -0,0 +1,239 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "stable": ["/"], + "positional": ["/Sheet1", "/Sheet2"] + }, + "note": "Add accepts name, position, autoFilter, tabColor, and hidden — these forward to the same code paths Set uses, preserving Add/Set symmetry. `freeze` remains Set-only.", + "properties": { + "name": { + "type": "string", + "description": "sheet tab name. Returned path is /; readback goes through DocumentNode.Path / .Preview rather than Format[].", + "add": true, "set": true, "get": true, + "examples": ["--prop name=Summary"], + "enforcement": "report" + }, + "autoFilter": { + "type": "string", + "description": "range to apply AutoFilter on (e.g. A1:D10). `true` enables on used range.", + "aliases": ["autofilter"], + "add": true, "set": true, "get": true, + "examples": ["--prop autoFilter=A1:D10"], + "readback": "range string as stored, or boolean true", + "enforcement": "report" + }, + "tabColor": { + "type": "color", + "description": "sheet tab color.", + "add": true, "set": true, "get": true, + "examples": ["--prop tabColor=4472C4"], + "readback": "#RRGGBB uppercase", + "enforcement": "report" + }, + "hidden": { + "type": "bool", + "description": "hide the sheet at creation or after the fact.", + "add": true, "set": true, "get": true, + "examples": ["--prop hidden=true"], + "enforcement": "report" + }, + "freeze": { + "type": "string", + "description": "freeze panes anchor (cell ref). A2 freezes row 1; B1 freezes column A; B2 freezes row 1 + column A. `none` / `false` / empty removes the freeze. Set-only on existing sheets.", + "add": false, "set": true, "get": true, + "examples": ["--prop freeze=A2", "--prop freeze=B2", "--prop freeze=none"], + "readback": "top-left cell ref of frozen pane (e.g. A2); absent when no freeze", + "enforcement": "report" + }, + "direction": { + "type": "enum", + "values": ["rtl", "ltr"], + "description": "RTL sheet layout (Arabic / Hebrew) — column A renders on the right, column scroll direction inverts. Maps to . Canonical key matches Word/PPT.", + "aliases": ["rtl", "rightToLeft", "righttoleft", "sheet.direction"], + "add": false, "set": true, "get": true, + "examples": ["--prop direction=rtl", "--prop rightToLeft=true"], + "readback": "rtl when set; absent when default (ltr)", + "enforcement": "report" + }, + "zoom": { + "type": "number", + "description": "sheetView zoom percentage (10-400). Emitted only when non-default (≠100).", + "add": false, "set": false, "get": true, + "readback": "zoom percentage 10-400", + "enforcement": "report" + }, + "gridlines": { + "type": "bool", + "description": "sheetView gridline visibility. Emitted only when hidden (false); default-on is omitted (CONSISTENCY(default-omission)).", + "add": false, "set": false, "get": true, + "readback": "true | false", + "enforcement": "report" + }, + "headings": { + "type": "bool", + "description": "row/column header visibility. Emitted only when hidden (false); default-on is omitted (CONSISTENCY(default-omission)).", + "add": false, "set": false, "get": true, + "readback": "row/column headings visible", + "enforcement": "report" + }, + "visibility": { + "type": "enum", + "values": ["hidden", "veryHidden"], + "description": "workbook-level sheet state when not visible. Emitted alongside hidden=true; absent for default-visible sheets.", + "add": false, "set": false, "get": true, + "readback": "if hidden", + "enforcement": "report" + }, + "protect": { + "type": "bool", + "description": "sheet protection state. On Set: pass `true` to enable protection, `false` to disable. Use the separate `password` property to set/clear an Excel legacy password hash.", + "add": false, "set": true, "get": true, + "readback": "true if sheet protection enabled", + "enforcement": "report" + }, + "password": { + "type": "string", + "description": "Excel legacy password hash for sheet protection (ECMA-376 14.7.1). On Set: pass plaintext password to hash and apply, or `none` to clear. Implicitly enables protection if not already set.", + "add": false, "set": true, "get": false, + "examples": ["--prop password=secret123", "--prop password=none"], + "readback": "n/a (hash not exposed on Get)", + "enforcement": "report" + }, + "printTitleRows": { + "type": "string", + "description": "rows to repeat at top of every printed page (e.g. 1:1).", + "add": false, "set": true, "get": false, + "examples": ["--prop printTitleRows=1:1"], + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "printTitleCols": { + "type": "string", + "description": "columns to repeat at left of every printed page (e.g. A:A).", + "add": false, "set": true, "get": false, + "examples": ["--prop printTitleCols=A:A"], + "readback": "n/a (set-only)", + "enforcement": "report" + }, + "orientation": { + "type": "string", + "description": "PageSetup orientation (portrait | landscape). Emitted only when set on the sheet.", + "add": false, "set": false, "get": true, + "readback": "page orientation (portrait|landscape)", + "enforcement": "report" + }, + "paperSize": { + "type": "number", + "description": "PageSetup paper-size code (OOXML enumeration; e.g. 1=Letter, 9=A4).", + "add": false, "set": false, "get": true, + "readback": "OOXML paper size code", + "enforcement": "report" + }, + "fitToPage": { + "type": "string", + "description": "PageSetup fit-to-page width x height (e.g. '1x1' = fit to one page).", + "add": false, "set": false, "get": true, + "readback": "WxH fit-to-page settings", + "enforcement": "report" + }, + "printArea": { + "type": "string", + "description": "defined-name _xlnm.Print_Area for this sheet. Get returns the A1 range with the leading 'SheetName!' prefix stripped. On Set: pass an A1 range (e.g. A1:C20) or `none` to clear.", + "add": false, "set": true, "get": true, + "examples": ["--prop printArea=A1:C20", "--prop printArea=none"], + "readback": "A1 range string", + "enforcement": "report" + }, + "margin.top": { + "type": "string", + "description": "PageMargins top margin in inches (e.g. '0.75in').", + "add": false, "set": false, "get": true, + "readback": "margin in inches", + "enforcement": "report" + }, + "margin.bottom": { + "type": "string", + "description": "PageMargins bottom margin in inches.", + "add": false, "set": false, "get": true, + "readback": "margin in inches", + "enforcement": "report" + }, + "margin.left": { + "type": "string", + "description": "PageMargins left margin in inches.", + "add": false, "set": false, "get": true, + "readback": "margin in inches", + "enforcement": "report" + }, + "margin.right": { + "type": "string", + "description": "PageMargins right margin in inches.", + "add": false, "set": false, "get": true, + "readback": "margin in inches", + "enforcement": "report" + }, + "margin.header": { + "type": "string", + "description": "PageMargins header margin in inches (distance from top edge to header).", + "add": false, "set": false, "get": true, + "readback": "margin in inches", + "enforcement": "report" + }, + "margin.footer": { + "type": "string", + "description": "PageMargins footer margin in inches (distance from bottom edge to footer).", + "add": false, "set": false, "get": true, + "readback": "margin in inches", + "enforcement": "report" + }, + "header": { + "type": "string", + "description": "odd-page header text (HeaderFooter/OddHeader). Excel format codes (&L, &C, &R, &P, &D, etc.) pass through verbatim.", + "add": false, "set": true, "get": true, + "readback": "raw odd-header text as stored", + "enforcement": "report" + }, + "footer": { + "type": "string", + "description": "odd-page footer text (HeaderFooter/OddFooter). Excel format codes pass through verbatim.", + "add": false, "set": true, "get": true, + "readback": "raw odd-footer text as stored", + "enforcement": "report" + }, + "sort": { + "type": "string", + "description": "sort the sheet by one or more columns. Set input: comma-separated `Col [dir]` tokens, direction optional, defaults to asc (e.g. `A`, `A asc`, `A asc,B desc`). Use `none` to clear. Get readback: comma-separated `Col:dir` entries (colon-separated, e.g. `A:asc`).", + "add": false, "set": true, "get": true, + "examples": ["--prop sort=A", "--prop sort=\"A asc,B desc\"", "--prop sort=none"], + "readback": "comma-separated `Col:asc|desc` list (e.g. `A:asc`)", + "enforcement": "report" + }, + "rowBreaks": { + "type": "string", + "description": "manual horizontal page breaks. Comma-separated row indices (1-based) where each break sits above that row.", + "add": false, "set": false, "get": true, + "readback": "comma-separated row break indices", + "enforcement": "report" + }, + "colBreaks": { + "type": "string", + "description": "manual vertical page breaks. Comma-separated column indices (1-based) where each break sits to the left of that column.", + "add": false, "set": false, "get": true, + "readback": "comma-separated column break indices", + "enforcement": "report" + } + }, + "children": [ + { "element": "cell", "pathSegment": "", "cardinality": "0..n" }, + { "element": "chart", "pathSegment": "chart", "cardinality": "0..n" } + ] +} diff --git a/schemas/help/xlsx/slicer.json b/schemas/help/xlsx/slicer.json new file mode 100644 index 000000000..bf699e4da --- /dev/null +++ b/schemas/help/xlsx/slicer.json @@ -0,0 +1,84 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "slicer", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/slicer[N]"] + }, + "note": "Slicers require an existing pivot table target. 'field' must match an existing cacheField name in the pivot's cache.", + "properties": { + "pivotTable": { + "type": "string", + "description": "path or reference to an existing pivot table. Bare names resolve against the host sheet's pivots.", + "aliases": ["pivot", "source", "tableName"], + "add": true, "set": true, "get": true, + "examples": ["--prop pivotTable='/Sheet1/pivottable[1]'", "--prop tableName=Pivot1"], + "readback": "pivot reference", + "enforcement": "report" + }, + "field": { + "type": "string", + "description": "pivot field name. Must match an existing cacheField (case-insensitive). Add-time only — Set ignores this key (slicers are anchored to their cache field at creation).", + "aliases": ["column"], + "add": true, "set": false, "get": true, + "examples": ["--prop field=Region", "--prop column=Region"], + "readback": "field name", + "enforcement": "report" + }, + "caption": { + "type": "string", + "description": "user-facing caption shown in the slicer header. Defaults to the field name.", + "add": true, "set": true, "get": true, + "examples": ["--prop caption='Filter by Region'"], + "readback": "caption", + "enforcement": "report" + }, + "name": { + "type": "string", + "description": "slicer name. Sanitized; defaults to 'Slicer_'.", + "add": true, "set": true, "get": true, + "examples": ["--prop name=RegionSlicer"], + "readback": "slicer name", + "enforcement": "report" + }, + "rowHeight": { + "type": "number", + "description": "row height of each slicer item, in EMU. Default 225425 (~17.5pt).", + "add": true, "set": true, "get": true, + "examples": ["--prop rowHeight=250000"], + "readback": "row height in EMU", + "enforcement": "report" + }, + "columnCount": { + "type": "number", + "description": "number of columns in the slicer button grid. Range 1..20000.", + "add": true, "set": true, "get": true, + "examples": ["--prop columnCount=2"], + "readback": "number of columns", + "enforcement": "report" + }, + "pivotCacheId": { + "type": "number", + "description": "extension pivot cache id (x14 cacheField extension). Read-only — auto-assigned at slicer creation.", + "add": false, "set": false, "get": true, + "readback": "pivot cache index (read-only)", + "enforcement": "report" + }, + "itemCount": { + "type": "number", + "description": "total number of items (buttons) in the slicer cache. Read-only — derived from the pivot's shared items.", + "add": false, "set": false, "get": true, + "readback": "total slicer item count", + "enforcement": "report" + }, + "cache": { "type":"string", "add":false, "set":false, "get":true, "description":"slicer cache name (Slicer @cache attribute).", "readback":"slicer cache name", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/sort.json b/schemas/help/xlsx/sort.json new file mode 100644 index 000000000..3ce860eca --- /dev/null +++ b/schemas/help/xlsx/sort.json @@ -0,0 +1,35 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "sort", + "parent": "sheet|range", + "operations": { + "add": false, + "set": true, + "get": true, + "query": false, + "remove": false + }, + "paths": { + "positional": ["/SheetName", "/SheetName/A1:D50"] + }, + "note": "Sort is Set-only — it mutates row order in a sheet or range. Sheet-level Set auto-detects the used range. Range-level Set operates only on the supplied range. SortState persists across save.", + "properties": { + "sort": { + "type": "string", + "description": "sort spec: 'COL [DIR][, COL [DIR] ...]'. COL is a column letter (A, B, AA..XFD). DIR is asc (default) or desc. Comma-separated for multi-key sort.", + "add": false, "set": true, "get": true, + "examples": ["--prop sort=B", "--prop sort=\"B desc, C asc\""], + "readback": "SortState description string", + "enforcement": "report" + }, + "sortHeader": { + "type": "bool", + "description": "treat first row as header (excluded from reorder).", + "add": false, "set": true, "get": false, + "examples": ["--prop sortHeader=true"], + "readback": "n/a", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/sparkline.json b/schemas/help/xlsx/sparkline.json new file mode 100644 index 000000000..30bc5eaf0 --- /dev/null +++ b/schemas/help/xlsx/sparkline.json @@ -0,0 +1,169 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "sparkline", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/sparkline[N]"] + }, + "note": "SparklineGroup stored under x14 extension list. Renders tiny inline chart in a target cell.", + "properties": { + "type": { + "type": "enum", + "values": ["line", "column", "stacked", "winloss", "win-loss"], + "description": "sparkline chart kind. 'stacked'/'winloss' both map to OOXML stacked.", + "add": true, "set": true, "get": true, + "examples": ["--prop type=line"], + "readback": "\"line\", \"column\", or \"winLoss\" (OOXML stacked maps back as \"winLoss\")", + "enforcement": "report" + }, + "dataRange": { + "type": "string", + "description": "source data range (e.g. A1:A10).", + "aliases": ["datarange", "range", "data"], + "add": true, "set": true, "get": true, + "examples": ["--prop dataRange=A1:A10"], + "readback": "range reference", + "enforcement": "report" + }, + "location": { + "type": "string", + "description": "target cell address.", + "aliases": ["cell", "ref"], + "add": true, "set": true, "get": true, + "examples": ["--prop location=B1"], + "readback": "target cell", + "enforcement": "report" + }, + "color": { + "type": "color", + "description": "series line/column color. Defaults to #4472C4.", + "add": true, "set": true, "get": true, + "examples": ["--prop color=#FF0000"], + "readback": "#RRGGBB", + "enforcement": "report" + }, + "negativeColor": { + "type": "color", + "description": "color used when 'negative' flag is on (winLoss/highlight negative points).", + "aliases": ["negativecolor"], + "add": true, "set": true, "get": true, + "examples": ["--prop negativeColor=#FF0000"], + "readback": "#RRGGBB", + "enforcement": "report" + }, + "markers": { + "type": "bool", + "description": "show data-point markers (line sparklines only).", + "add": true, "set": true, "get": true, + "examples": ["--prop markers=true"], + "readback": "true/false", + "enforcement": "report" + }, + "highPoint": { + "type": "bool", + "description": "highlight the maximum point.", + "aliases": ["highpoint"], + "add": true, "set": true, "get": true, + "examples": ["--prop highPoint=true"], + "readback": "true/false", + "enforcement": "report" + }, + "lowPoint": { + "type": "bool", + "description": "highlight the minimum point.", + "aliases": ["lowpoint"], + "add": true, "set": true, "get": true, + "examples": ["--prop lowPoint=true"], + "readback": "true/false", + "enforcement": "report" + }, + "firstPoint": { + "type": "bool", + "description": "highlight the first point.", + "aliases": ["firstpoint"], + "add": true, "set": true, "get": true, + "examples": ["--prop firstPoint=true"], + "readback": "true/false", + "enforcement": "report" + }, + "lastPoint": { + "type": "bool", + "description": "highlight the last point.", + "aliases": ["lastpoint"], + "add": true, "set": true, "get": true, + "examples": ["--prop lastPoint=true"], + "readback": "true/false", + "enforcement": "report" + }, + "negative": { + "type": "bool", + "description": "highlight negative points using negativeColor.", + "add": true, "set": true, "get": true, + "examples": ["--prop negative=true"], + "readback": "true/false", + "enforcement": "report" + }, + "highMarkerColor": { + "type": "color", + "description": "marker color for the high point. Add-only; not modifiable via Set.", + "aliases": ["highmarkercolor"], + "add": true, "set": false, "get": false, + "examples": ["--prop highMarkerColor=#00B050"], + "readback": "n/a", + "enforcement": "report" + }, + "lowMarkerColor": { + "type": "color", + "description": "marker color for the low point. Add-only.", + "aliases": ["lowmarkercolor"], + "add": true, "set": false, "get": false, + "examples": ["--prop lowMarkerColor=#FF0000"], + "readback": "n/a", + "enforcement": "report" + }, + "firstMarkerColor": { + "type": "color", + "description": "marker color for the first point. Add-only.", + "aliases": ["firstmarkercolor"], + "add": true, "set": false, "get": false, + "examples": ["--prop firstMarkerColor=#4472C4"], + "readback": "n/a", + "enforcement": "report" + }, + "lastMarkerColor": { + "type": "color", + "description": "marker color for the last point. Add-only.", + "aliases": ["lastmarkercolor"], + "add": true, "set": false, "get": false, + "examples": ["--prop lastMarkerColor=#4472C4"], + "readback": "n/a", + "enforcement": "report" + }, + "markersColor": { + "type": "color", + "description": "marker color for all non-extreme points. Add-only.", + "aliases": ["markerscolor"], + "add": true, "set": false, "get": false, + "examples": ["--prop markersColor=#808080"], + "readback": "n/a", + "enforcement": "report" + }, + "lineWeight": { + "type": "number", + "description": "line stroke weight in points (line sparklines only).", + "aliases": ["lineweight"], + "add": true, "set": true, "get": true, + "examples": ["--prop lineWeight=1.5"], + "readback": "number", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/table.json b/schemas/help/xlsx/table.json new file mode 100644 index 000000000..8869470ee --- /dev/null +++ b/schemas/help/xlsx/table.json @@ -0,0 +1,202 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "table", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": [ + "/SheetName/table[N]" + ] + }, + "note": "Aliases: listobject. 'ref' (alias 'range') required: cell range like 'A1:C10'. Rejects ranges that overlap existing tables. Names sanitized; style validated against built-in/custom whitelist.", + "extends": [ + "_shared/table", + "_shared/table.pptx-xlsx" + ], + "properties": { + "ref": { + "type": "string", + "description": "cell range reference (A1:C10). Required. Alias: range.", + "aliases": [ + "range" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop ref=A1:C10" + ], + "readback": "range string", + "enforcement": "report" + }, + "displayName": { + "type": "string", + "description": "Excel UI display name. Defaults to name.", + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop displayName=SalesData" + ], + "readback": "display name", + "enforcement": "report" + }, + "headerRow": { + "type": "bool", + "description": "show header row. Alias: showHeader.", + "aliases": [ + "showHeader" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop headerRow=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "totalRow": { + "type": "bool", + "description": "show total row. Alias: showTotals.", + "aliases": [ + "showTotals" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop totalRow=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "autoExpand": { + "type": "bool", + "description": "auto-expand range downward through contiguous non-empty rows at Add time.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop autoExpand=true" + ], + "readback": "affects range at Add time", + "enforcement": "report" + }, + "showFirstColumn": { + "type": "bool", + "description": "highlight the first column with the table style. Alias: firstColumn.", + "aliases": [ + "firstColumn" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop showFirstColumn=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "showLastColumn": { + "type": "bool", + "description": "highlight the last column with the table style. Alias: lastColumn.", + "aliases": [ + "lastColumn" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop showLastColumn=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "showRowStripes": { + "type": "bool", + "description": "alternate-row banding from the table style. Default: true. Aliases: showBandedRows, bandedRows, bandRows.", + "aliases": [ + "showBandedRows", + "bandedRows", + "bandRows" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop showRowStripes=false" + ], + "readback": "true/false", + "enforcement": "report" + }, + "showColumnStripes": { + "type": "bool", + "description": "alternate-column banding from the table style. Default: false. Aliases: showBandedColumns, bandedColumns, bandedCols, showColStripes, bandCols.", + "aliases": [ + "showBandedColumns", + "bandedColumns", + "bandedCols", + "showColStripes", + "bandCols" + ], + "add": true, + "set": true, + "get": true, + "examples": [ + "--prop showColumnStripes=true" + ], + "readback": "true/false", + "enforcement": "report" + }, + "columns": { + "type": "string", + "description": "comma-separated column header names overriding A1, B1, ... defaults.", + "add": true, + "set": false, + "get": true, + "examples": [ + "--prop columns=Name,Qty,Price" + ], + "readback": "comma-separated column names as stored (e.g. \"Name,Qty,Price\")", + "enforcement": "report" + }, + "totalsRowFunction": { + "type": "string", + "description": "comma-separated per-column totals row functions (none|sum|average|count|countNums|max|min|stdDev|var|custom). Effective only when totalRow=true.", + "add": true, + "set": false, + "get": false, + "examples": [ + "--prop totalsRowFunction=none,sum,average" + ], + "readback": "per-column tokens", + "enforcement": "report" + }, + "totalFunction": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "per-column totals-row function readback (surfaces on the column child node).", + "readback": "function token", + "enforcement": "report" + }, + "totalLabel": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "per-column totals-row label readback (surfaces on the column child node).", + "readback": "label text", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/topn.json b/schemas/help/xlsx/topn.json new file mode 100644 index 000000000..777c83bdb --- /dev/null +++ b/schemas/help/xlsx/topn.json @@ -0,0 +1,21 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "topn", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Top-N or bottom-N rank conditional formatting. Add via `add /Sheet1/cf --type topn --prop sqref=A1:A100 --prop rank=10`. Aliases for type: top10, top. Lookup: Add.Cf.cs:577 (AddCfExtended `topn` case); Get: Query.cs:545.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A100"], "enforcement":"report" }, + "rank": { "type":"number", "aliases":["top","bottomN","value"], "add":true, "get":true, "description":"number (or percent) of items to highlight. Default 10. Required >= 1.", "examples":["--prop rank=10"], "readback":"integer", "enforcement":"report" }, + "percent": { "type":"bool", "add":true, "get":true, "description":"interpret rank as a percentage (true) or absolute count (false, default).", "examples":["--prop percent=true"], "readback":"true | false (only emitted when true)", "enforcement":"report" }, + "bottom": { "type":"bool", "add":true, "get":true, "description":"highlight bottom-N instead of top-N (default false).", "examples":["--prop bottom=true"], "readback":"true | false (only emitted when true)", "enforcement":"report" }, + "fill": { "type":"color", "add":true, "get":false, "description":"background fill via dxf.", "examples":["--prop fill=FFFF00"], "enforcement":"report" }, + "font.color": { "type":"color", "add":true, "get":false, "description":"font color via dxf.", "examples":["--prop font.color=FF0000"], "enforcement":"report" }, + "font.bold": { "type":"bool", "add":true, "get":false, "description":"bold via dxf.", "examples":["--prop font.bold=true"], "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/uniquevalues.json b/schemas/help/xlsx/uniquevalues.json new file mode 100644 index 000000000..0fb253d6b --- /dev/null +++ b/schemas/help/xlsx/uniquevalues.json @@ -0,0 +1,18 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "uniquevalues", + "parent": "sheet", + "operations": {"add": true, "get": true}, + "paths": {"positional": ["/SheetName/cf[N]"]}, + "note": "Highlight unique values in range. Add via `add /Sheet1/cf --type uniquevalues --prop sqref=A1:A100 --prop fill=FFFF00`. Lookup: Add.Cf.cs:637 (AddCfExtended `uniquevalues` case); Get: Query.cs:570.", + "properties": { + "ref": { "type":"string", "aliases":["sqref","range"], "add":true, "get":true, "description":"target cell range.", "examples":["--prop ref=A1:A100"], "enforcement":"report" }, + "fill": { "type":"color", "add":true, "get":false, "description":"background fill via dxf.", "examples":["--prop fill=FFFF00"], "enforcement":"report" }, + "font.color": { "type":"color", "add":true, "get":false, "description":"font color via dxf.", "examples":["--prop font.color=FF0000"], "enforcement":"report" }, + "font.bold": { "type":"bool", "add":true, "get":false, "description":"bold via dxf.", "examples":["--prop font.bold=true"], "enforcement":"report" }, + "stopIfTrue": { "type":"bool", "add":true, "get":false, "description":"stop evaluating subsequent CF rules when this rule applies.", "examples":["--prop stopIfTrue=true"], "enforcement":"report" }, + "type": { "type":"string", "add":false, "set":false, "get":true, "description":"canonical CF rule type token (e.g. \"dataBar\", \"colorScale\", \"iconSet\", \"formula\", \"topN\", \"cellIs\", \"containsText\", \"aboveAverage\", \"duplicateValues\", \"uniqueValues\", \"timePeriod\"). Emitted on every CF rule.", "readback":"normalized CF type token", "enforcement":"report" }, + "dxfId": { "type":"number", "add":false, "set":false, "get":true, "description":"differential format id referencing dxf styles. Emitted only when present on the rule.", "readback":"integer", "enforcement":"report" } + } +} diff --git a/schemas/help/xlsx/validation.json b/schemas/help/xlsx/validation.json new file mode 100644 index 000000000..f8796a3dc --- /dev/null +++ b/schemas/help/xlsx/validation.json @@ -0,0 +1,139 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "validation", + "parent": "sheet", + "operations": { + "add": true, + "set": true, + "get": true, + "query": true, + "remove": true + }, + "paths": { + "positional": ["/SheetName/dataValidation[N]"] + }, + "note": "Aliases: datavalidation. Target cell range via 'ref'. Type determines which of formula1/formula2 are used. Alias 'validation' accepted in path segments by query/set/remove (e.g. /SheetName/validation[N]); Add and Get echo back the canonical 'dataValidation[N]' form.", + "properties": { + "type": { + "type": "enum", + "values": ["list", "whole", "decimal", "date", "time", "textlength", "custom"], + "add": true, "set": true, "get": true, + "examples": ["--prop type=list"], + "readback": "validation type", + "enforcement": "report" + }, + "ref": { + "type": "string", + "description": "target cell range. Aliases: sqref.", + "aliases": ["sqref"], + "add": true, "set": true, "get": true, + "examples": ["--prop ref=A1:A10", "--prop sqref=A1:A10"], + "readback": "cell range", + "enforcement": "report" + }, + "allowBlank": { + "type": "bool", + "description": "allow blank cells. Default: true.", + "add": true, "set": true, "get": true, + "examples": ["--prop allowBlank=false"], + "readback": "true/false", + "enforcement": "report" + }, + "showError": { + "type": "bool", + "description": "show error message on invalid input. Default: true.", + "add": true, "set": true, "get": true, + "examples": ["--prop showError=true"], + "readback": "true/false", + "enforcement": "report" + }, + "showInput": { + "type": "bool", + "description": "show input prompt when cell selected. Default: true.", + "add": true, "set": true, "get": true, + "examples": ["--prop showInput=true"], + "readback": "true/false", + "enforcement": "report" + }, + "errorTitle": { + "type": "string", + "description": "title of the error popup.", + "add": true, "set": true, "get": true, + "examples": ["--prop errorTitle=\"Bad value\""], + "readback": "title text", + "enforcement": "report" + }, + "promptTitle": { + "type": "string", + "description": "title of the input prompt popup.", + "add": true, "set": true, "get": true, + "examples": ["--prop promptTitle=\"Hint\""], + "readback": "title text", + "enforcement": "report" + }, + "errorStyle": { + "type": "enum", + "values": ["stop", "warning", "information"], + "description": "severity of error popup. Default: stop. Aliases: warn=warning, info=information.", + "add": true, "set": false, "get": true, + "examples": ["--prop errorStyle=warning"], + "readback": "stop|warning|information", + "enforcement": "report" + }, + "inCellDropdown": { + "type": "bool", + "description": "show in-cell dropdown arrow for type=list. Default: true. Inverse of OOXML showDropDown.", + "add": true, "set": false, "get": true, + "examples": ["--prop inCellDropdown=false"], + "readback": "true/false", + "enforcement": "report" + }, + "showDropDown": { + "type": "bool", + "description": "raw OOXML showDropDown flag (INVERTED: true = HIDE arrow). Prefer inCellDropdown for clarity.", + "add": true, "set": false, "get": false, + "examples": ["--prop showDropDown=true"], + "readback": "raw OOXML flag", + "enforcement": "report" + }, + "operator": { + "type": "enum", + "values": ["between", "notBetween", "equal", "notEqual", "greaterThan", "greaterThanOrEqual", "lessThan", "lessThanOrEqual"], + "add": true, "set": true, "get": true, + "examples": ["--prop operator=between"], + "readback": "operator name", + "enforcement": "report" + }, + "formula1": { + "type": "string", + "add": true, "set": true, "get": true, + "examples": ["--prop formula1=\"Yes,No,Maybe\""], + "readback": "formula1 content", + "enforcement": "report" + }, + "formula2": { + "type": "string", + "add": true, "set": true, "get": true, + "examples": ["--prop formula2=100"], + "readback": "formula2 content", + "enforcement": "report" + }, + "prompt": { + "type": "string", + "description": "message shown when cell selected.", + "add": true, "set": true, "get": true, + "examples": ["--prop prompt=\"Enter 1-100\""], + "readback": "prompt text", + "enforcement": "report" + }, + "error": { + "type": "string", + "description": "error message on invalid input.", + "add": true, "set": true, "get": true, + "examples": ["--prop error=\"Invalid value\""], + "readback": "error text", + "enforcement": "report" + } + } +} diff --git a/schemas/help/xlsx/workbook.json b/schemas/help/xlsx/workbook.json new file mode 100644 index 000000000..02786cfd0 --- /dev/null +++ b/schemas/help/xlsx/workbook.json @@ -0,0 +1,394 @@ +{ + "$schema": "../_schema.json", + "format": "xlsx", + "element": "workbook", + "container": true, + "operations": { + "add": false, + "set": true, + "get": true, + "query": true, + "remove": false + }, + "paths": { + "positional": [ + "/" + ] + }, + "note": "Root container. Get returns sheet list and workbook-level metadata. Set exists for workbook-wide properties (defaultFont, defaultFontSize, calc.mode, calc.iterate, author, title, subject). Sheets are mutated via /SheetName paths.", + "children": [ + { + "element": "sheet", + "pathSegment": "{SheetName}", + "cardinality": "1..n" + } + ], + "extends": "_shared/root-metadata", + "properties": { + "defaultFont": { + "type": "string", + "description": "default font for all cells (fontname alias). Not implemented in ExcelHandler — Add/Set/Get all return n/a today; manage cell fonts directly.", + "aliases": [ + "fontName", + "fontname" + ], + "add": false, + "set": false, + "get": false, + "examples": [ + "--prop defaultFont=Calibri" + ], + "readback": "n/a", + "enforcement": "report" + }, + "defaultFontSize": { + "type": "length", + "description": "default font size. Not implemented in ExcelHandler — Add/Set/Get all return n/a today; manage cell fonts directly.", + "aliases": [ + "fontSize", + "fontsize" + ], + "add": false, + "set": false, + "get": false, + "examples": [ + "--prop defaultFontSize=11" + ], + "readback": "n/a", + "enforcement": "report" + }, + "author": { + "type": "string", + "description": "document author (core properties).", + "aliases": [ + "creator" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop author=\"Alice\"" + ], + "readback": "author string", + "enforcement": "report" + }, + "title": { + "type": "string", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop title=\"Q1 Report\"" + ], + "readback": "title string", + "enforcement": "report" + }, + "calc.mode": { + "type": "enum", + "values": [ + "auto", + "manual", + "autoExceptTables" + ], + "description": "workbook formula calculation mode. 'auto' recalculates on every change, 'manual' requires F9, 'autoExceptTables' skips data tables.", + "aliases": [ + "calcmode" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop calc.mode=manual", + "--prop calcmode=auto" + ], + "readback": "calc mode name", + "enforcement": "report" + }, + "calc.iterate": { + "type": "bool", + "description": "enable iterative calculation for circular references.", + "aliases": [ + "iterate" + ], + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop calc.iterate=true", + "--prop iterate=false" + ], + "readback": "true|false", + "enforcement": "report" + }, + "calc.iterateCount": { + "type": "number", + "description": "maximum number of iterations when calc.iterate is enabled.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop calc.iterateCount=100" + ], + "readback": "integer", + "enforcement": "report" + }, + "calc.iterateDelta": { + "type": "number", + "description": "maximum change between iterations to consider the calculation converged.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop calc.iterateDelta=0.001" + ], + "readback": "number", + "enforcement": "report" + }, + "calc.fullPrecision": { + "type": "bool", + "description": "if true, calculations use full precision rather than the displayed value.", + "add": false, + "set": true, + "get": true, + "examples": [ + "--prop calc.fullPrecision=true" + ], + "readback": "true|false", + "enforcement": "report" + }, + "lastModifiedBy": { + "type": "string", + "aliases": [ + "lastmodifiedby" + ], + "add": false, + "set": true, + "get": true, + "description": "from docProps/core.xml lastModifiedBy field.", + "examples": [ + "--prop lastModifiedBy=\"Alice\"" + ], + "readback": "author string", + "enforcement": "report" + }, + "created": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "creation timestamp ISO-8601.", + "readback": "ISO-8601 timestamp", + "enforcement": "report" + }, + "modified": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "last modification timestamp ISO-8601.", + "readback": "ISO-8601 timestamp", + "enforcement": "report" + }, + "extended.application": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "from docProps/app.xml application identifier (e.g. \"Microsoft Excel\").", + "readback": "application string", + "enforcement": "report" + }, + "category": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "docProps/core.xml Category field.", + "examples": [ + "--prop category=Reports" + ], + "readback": "category string", + "enforcement": "report" + }, + "description": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "docProps/core.xml Description field.", + "examples": [ + "--prop description=\"Annual revenue summary\"" + ], + "readback": "description string", + "enforcement": "report" + }, + "keywords": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "docProps/core.xml Keywords field.", + "examples": [ + "--prop keywords=\"finance,2026\"" + ], + "readback": "keyword list string", + "enforcement": "report" + }, + "revisionNumber": { + "type": "string", + "add": false, + "set": true, + "get": true, + "description": "docProps/core.xml Revision field — workbook save counter.", + "examples": [ + "--prop revisionNumber=3" + ], + "readback": "revision number string", + "enforcement": "report" + }, + "activeTab": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "workbook BookViews activeTab — index of the sheet active when the file opens.", + "readback": "integer (0-based)", + "enforcement": "report" + }, + "firstSheet": { + "type": "number", + "add": false, + "set": false, + "get": true, + "description": "workbook BookViews firstSheet — index of the leftmost visible sheet.", + "readback": "integer (0-based)", + "enforcement": "report" + }, + "calc.fullCalcOnLoad": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "CalculationProperties FullCalculationOnLoad flag — force a full recalc when the workbook opens.", + "readback": "true|false", + "enforcement": "report" + }, + "calc.refMode": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "CalculationProperties ReferenceMode (A1 or R1C1).", + "readback": "reference mode string", + "enforcement": "report" + }, + "workbook.backupFile": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "WorkbookProperties BackupFile flag — Excel keeps a backup .bak alongside saves.", + "readback": "true|false", + "enforcement": "report" + }, + "workbook.codeName": { + "type": "string", + "add": false, + "set": false, + "get": true, + "description": "WorkbookProperties CodeName — VBA project workbook codename (e.g. ThisWorkbook).", + "readback": "codename string", + "enforcement": "report" + }, + "workbook.date1904": { + "type": "bool", + "add": false, + "set": false, + "get": true, + "description": "WorkbookProperties Date1904 flag — true means dates use the 1904 epoch (Mac legacy).", + "readback": "true|false", + "enforcement": "report" + }, + "workbook.dateCompatibility": { + "type": "bool", + "aliases": ["datecompatibility"], + "add": false, + "set": true, + "get": true, + "description": "WorkbookProperties DateCompatibility flag — controls 1900 vs 1904 date system compatibility.", + "examples": [ + "--prop workbook.dateCompatibility=true" + ], + "readback": "true|false", + "enforcement": "report" + }, + "workbook.filterPrivacy": { + "type": "bool", + "aliases": ["filterprivacy"], + "add": false, + "set": true, + "get": true, + "description": "WorkbookProperties FilterPrivacy flag — when true, Excel hides personal info from filter saves.", + "examples": [ + "--prop workbook.filterPrivacy=true" + ], + "readback": "true|false", + "enforcement": "report" + }, + "workbook.showObjects": { + "type": "enum", + "values": ["all", "placeholders", "none"], + "aliases": ["showobjects"], + "add": false, + "set": true, + "get": true, + "description": "WorkbookProperties ShowObjects — visibility of embedded objects (charts, pictures, shapes) in the workbook view.", + "examples": [ + "--prop workbook.showObjects=all", + "--prop workbook.showObjects=none" + ], + "readback": "all|placeholders|none", + "enforcement": "report" + }, + "workbook.lockStructure": { + "type": "bool", + "aliases": ["lockstructure"], + "add": false, + "set": true, + "get": true, + "description": "WorkbookProtection LockStructure flag — when true, sheets cannot be added/deleted/renamed/reordered.", + "examples": [ + "--prop workbook.lockStructure=true" + ], + "readback": "true|false", + "enforcement": "report" + }, + "workbook.lockWindows": { + "type": "bool", + "aliases": ["lockwindows"], + "add": false, + "set": true, + "get": true, + "description": "WorkbookProtection LockWindows flag — when true, workbook window size and position are locked.", + "examples": [ + "--prop workbook.lockWindows=true" + ], + "readback": "true|false", + "enforcement": "report" + }, + "workbook.password": { + "type": "string", + "aliases": ["workbookpassword"], + "add": false, + "set": true, + "get": true, + "description": "WorkbookProtection legacy password (ECMA-376 short hash). Set the plaintext to apply; pass empty or 'none' to clear. Get returns '***' when present (the plaintext is not recoverable from the stored hash). Known weak — back-compat only.", + "examples": [ + "--prop workbook.password=secret", + "--prop workbook.password=none" + ], + "readback": "*** if set, otherwise omitted", + "enforcement": "report" + } + } +} diff --git a/skills/morph-ppt-3d/SKILL.md b/skills/morph-ppt-3d/SKILL.md index d3d641664..75f180bb7 100644 --- a/skills/morph-ppt-3d/SKILL.md +++ b/skills/morph-ppt-3d/SKILL.md @@ -10,6 +10,15 @@ This file covers **3D-specific additions** and an **enriched design system** com --- +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. + ## Use when - User wants a `.pptx` with a `.glb` 3D model and Morph transitions. @@ -32,18 +41,18 @@ When the user gives a topic but no `.glb` file, **proactively help them find a m Based on the user's topic, suggest what kind of 3D model would work: -| Topic type | Model suggestion | Example | -| ------------------ | ----------------------------------- | ---------------------------------------------------- | -| Product/brand | The actual product or a similar one | "咖啡品牌" → coffee cup, coffee machine, coffee bean | -| Animal/character | The animal or mascot | "柴犬介绍" → shiba inu dog model | -| Architecture/space | Building, room, or structure | "新办公室" → office building, interior | -| Vehicle/transport | The vehicle itself | "电动车发布" → car, motorcycle, bicycle | -| Food/cooking | The dish or ingredient | "日料介绍" → sushi platter, ramen bowl | -| Tech/gadget | The device | "新手机发布" → phone, tablet, laptop | -| Nature/science | The subject | "太阳系" → planet, sun, earth | -| Abstract concept | A symbolic object | "团队合作" → puzzle pieces, gears, bridge | +| Topic type | Model suggestion | Example | +| ------------------ | ----------------------------------- | ----------------------------------------------------- | +| Product/brand | The actual product or a similar one | "coffee brand" → coffee cup, coffee machine, bean | +| Animal/character | The animal or mascot | "fox mascot" → fox 3D model | +| Architecture/space | Building, room, or structure | "new office" → office building, interior | +| Vehicle/transport | The vehicle itself | "EV launch" → car, motorcycle, bicycle | +| Food/cooking | The dish or ingredient | "Japanese food" → sushi platter, ramen bowl | +| Tech/gadget | The device | "phone launch" → phone, tablet, laptop | +| Nature/science | The subject | "solar system" → planet, sun, earth | +| Abstract concept | A symbolic object | "teamwork" → puzzle pieces, gears, bridge | -Tell the user: "你的主题是 [X],建议用 [具体模型描述] 的 3D 模型。我推荐几个免费下载来源:" +Tell the user: "Your topic is [X]. I suggest using a 3D model of [description]. Here are some free sources to find one:" ### Step 2: Search for models (agent-driven) @@ -55,7 +64,7 @@ Tell the user: "你的主题是 [X],建议用 [具体模型描述] 的 3D 模 ``` Search: "[topic keyword] 3d model glb free download" - Example: "shiba dog 3d model glb free download" + Example: "fox 3d model glb free download" ``` 2. **Sketchfab API** (no auth needed for search): @@ -99,23 +108,23 @@ Show the user 2-3 model options with: Example response: ``` -根据你的主题"柴犬品牌",我找到了这些模型: +Based on your topic "fox mascot", here are some models I found: -1. 🐕 Shiba Inu (Sketchfab) - 链接:https://sketchfab.com/3d-models/shiba-xxx - 授权:CC BY 4.0(免费可商用) - 推荐理由:高质量柴犬模型,表情可爱 +1. Fox (Khronos sample) + Direct download, guaranteed compatible + Why: clean fox model, good for mascot/character decks -2. 🐶 Low Poly Dog (Poly Pizza) - 链接:https://poly.pizza/m/xxx - 授权:CC0(完全免费) - 推荐理由:低多边形风格,适合简洁设计 +2. Low Poly Fox (Poly Pizza) + URL: https://poly.pizza/m/xxx + License: CC0 (completely free) + Why: low-poly style, good fit for clean minimal design -3. 🦊 Fox (Khronos 官方样例) - 可直接下载,保证兼容 - 推荐理由:狐狸和柴犬外形相似,作为备选 +3. Cartoon Fox (Sketchfab) + URL: https://sketchfab.com/3d-models/fox-xxx + License: CC BY 4.0 (free, commercial use ok) + Why: expressive face, high detail -你选哪个?确认后我直接下载开始做。 +Which one do you want? I'll download it and start building. ``` **Wait for user confirmation before downloading.** Do not download without asking. @@ -140,90 +149,69 @@ After download, verify: If Sketchfab requires login to download, tell the user: -> "这个模型需要在 Sketchfab 登录后下载。你可以去页面下载 .glb 文件,然后上传给我。或者我用 Khronos 官方样例先做一版演示?" +> "This model requires a Sketchfab login to download. You can grab the .glb file from the page and share it with me. Or I can use a Khronos sample model for a demo version first?" -### Step 5: When user says "随便" / "你定" / "先做个演示" +### Step 5: When user says "anything" / "you decide" / "just make a demo" **Don't just grab a random model.** First guide the user to clarify their PPT topic: -> 好的!模型我来搞定,但先确认一下你的 PPT 主题方向,这样我找的模型才能配合内容: +> Sure! I'll handle the model — but let me confirm the topic direction first so the model matches the content: > -> 1. 🎮 科技/产品 — 耳机、手机、机器人... -> 2. 🐾 动物/角色 — 可爱宠物、卡通人物... -> 3. 🏗️ 建筑/空间 — 房屋、室内、城市... -> 4. 🍕 食物/生活 — 美食、日用品... -> 5. 🚀 其他 — 告诉我你的想法 +> 1. Tech/Product — headphones, phone, robot... +> 2. Animal/Character — cute pet, cartoon character... +> 3. Architecture/Space — building, interior, city... +> 4. Food/Lifestyle — dishes, everyday objects... +> 5. Other — just tell me your idea > -> 选一个方向,或者直接说个主题词也行。 +> Pick a direction, or just give me a topic keyword. After user confirms a direction, THEN search and recommend models. -Only if user explicitly says "真的随便" / "什么都行" / insists on no preference, use a built-in model: - -**Built-in models** (bundled with the skill, no download needed): - -| Model | Path | Best for | -| ---------------- | ------------------ | ----------------------- | -| Shiba Inu (柴犬) | `models/shiba.glb` | 可爱/宠物/品牌/通用演示 | - -```bash -# Copy built-in model to working directory -# The model is bundled at: skills/morph-ppt-3d/models/shiba.glb (relative to officecli repo root) -cp "$(dirname "$0")/models/shiba.glb" ./model.glb 2>/dev/null || \ - curl -L -o model.glb "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/Duck/glTF-Binary/Duck.glb" -``` - -If the built-in model doesn't fit the user's topic, fall back to Khronos samples: - -```bash -curl -L -o model.glb "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/Duck/glTF-Binary/Duck.glb" -``` - ### Step 6: When user wants to find models themselves Give specific website links with step-by-step guidance: -> **推荐的 3D 模型网站:** +> **Recommended 3D model websites:** > -> 1. **Sketchfab** (最大的 3D 模型平台) -> - 链接:https://sketchfab.com/search?q=[关键词]&type=models&downloadable=true -> - 筛选步骤:搜索关键词 → 勾选 "Downloadable" → 格式选 "glTF" → 按 "Likes" 排序 -> - 下载时选 **glTF (.glb)** 格式 -> - 注意:部分模型需要免费注册后才能下载 -> 2. **Poly Pizza** (全免费低多边形) -> - 链接:https://poly.pizza/ -> - 特点:全部免费 CC0 授权,直接点 Download 就是 .glb -> - 适合:简约风格、卡通风格的 PPT -> 3. **Sketchfab 热门分类直达** -> - 动物:https://sketchfab.com/search?q=animal&type=models&downloadable=true -> - 食物:https://sketchfab.com/search?q=food&type=models&downloadable=true -> - 科技:https://sketchfab.com/search?q=gadget&type=models&downloadable=true -> - 建筑:https://sketchfab.com/search?q=architecture&type=models&downloadable=true -> 4. **Free3D** (综合免费模型站) -> - 链接:https://free3d.com/3d-models/glb -> - 注意:需确认授权类型 -> 5. **TurboSquid Free** (专业模型站免费区) -> - 链接:https://www.turbosquid.com/Search/3D-Models/free/glb +> 1. **Sketchfab** (largest 3D model platform) +> - Link: https://sketchfab.com/search?q=[keyword]&type=models&downloadable=true +> - Filter steps: search keyword → check "Downloadable" → format "glTF" → sort by "Likes" +> - When downloading, select **glTF (.glb)** format +> - Note: some models require free registration to download +> 2. **Poly Pizza** (all free low-poly) +> - Link: https://poly.pizza/ +> - All CC0 licensed — click Download to get .glb directly +> - Best for: minimalist or cartoon-style presentations +> 3. **Sketchfab popular categories** +> - Animals: https://sketchfab.com/search?q=animal&type=models&downloadable=true +> - Food: https://sketchfab.com/search?q=food&type=models&downloadable=true +> - Tech: https://sketchfab.com/search?q=gadget&type=models&downloadable=true +> - Architecture: https://sketchfab.com/search?q=architecture&type=models&downloadable=true +> 4. **Free3D** (general free model site) +> - Link: https://free3d.com/3d-models/glb +> - Note: check the license type before use +> 5. **TurboSquid Free** (pro model site free section) +> - Link: https://www.turbosquid.com/Search/3D-Models/free/glb > -> 下载后把 .glb 文件发给我就行。如果下载的是 .gltf(文件夹),需要用 Blender 转成 .glb。 +> After downloading, share the .glb file with me. If the download is a .gltf folder, use Blender to convert it to .glb. ### Step 7: When user gives keywords and asks agent to search **Remind about token cost before searching:** -> 我可以帮你搜索,不过在线搜索会消耗一些额外的对话额度 (token)。你想: +> I can search for you, but web searches use extra tokens. Would you prefer: > -> A. 我来搜 — 我用 Sketchfab API 搜索并推荐 2-3 个(消耗少量 token) -> B. 你自己找 — 我给你搜索链接和筛选教程,你挑好发给我(不消耗额外 token) +> A. I search — I use the Sketchfab API and recommend 2-3 options (uses a few tokens) +> B. Self-service — I give you search links and filter steps, you pick and share with me (no extra tokens) > -> 选 A 还是 B? +> A or B? If user chooses A, proceed with Step 2 (agent-driven search). If user chooses B, proceed with Step 6 (self-service guidance). ### License reminder -Always remind before confirming download: "下载前请确认模型授权。CC0 / CC BY 可免费使用;CC BY-NC 仅限非商用。" +Always remind before confirming download: "Please check the model license before downloading. CC0 / CC BY = free to use; CC BY-NC = non-commercial only." --- @@ -463,7 +451,7 @@ Bleed does NOT work for: - ❌ Small detailed models — cropping loses the detail you want to show - ❌ When the cropped part is the most recognizable feature -**For character/animal models (like shiba, fox, duck):** keep the full model visible on all slides. Use size changes (L→M→S) for rhythm instead of bleed cropping. Use `rotx` for angle variety instead. +**For character/animal models (like fox, duck, avocado):** keep the full model visible on all slides. Use size changes (L→M→S) for rhythm instead of bleed cropping. Use `rotx` for angle variety instead. --- diff --git a/skills/morph-ppt-3d/models/shiba.glb b/skills/morph-ppt-3d/models/shiba.glb deleted file mode 100644 index 5484d56e7..000000000 Binary files a/skills/morph-ppt-3d/models/shiba.glb and /dev/null differ diff --git a/skills/morph-ppt/SKILL.md b/skills/morph-ppt/SKILL.md index 63f50b996..e2bdb7954 100644 --- a/skills/morph-ppt/SKILL.md +++ b/skills/morph-ppt/SKILL.md @@ -1,521 +1,536 @@ --- name: morph-ppt -description: Generate Morph-animated PPTs with officecli +description: "Use this skill when the user wants a .pptx with smooth cross-slide animation — PowerPoint Morph transitions, Keynote-style continuous motion, shapes that grow / move / rotate as the slide advances. Trigger on: 'morph', 'morph transition', 'smooth transition', 'continuous animation across slides', 'Keynote-style transition', 'animated slide sequence', 'shape continuity across slides'. Output is a single .pptx. This skill is a scene layer on top of officecli-pptx — inherits every pptx v2 rule (visual floor, grid, palettes, connector canon, Delivery Gate 1–5a). DO NOT invoke for a generic deck, pitch deck, or board review without cross-slide motion — route those to officecli-pptx base or officecli-pitch-deck." --- -# Morph +# OfficeCLI Morph-PPT Skill -Generate visually compelling PPTs with smooth Morph animations. +**This skill is a scene layer on top of `officecli-pptx`.** Every pptx hard rule — visual delivery floor (title ≥ 36pt / body ≥ 18pt / title ≥ 2× body), 12-column grid on 33.87×19.05cm, canonical palettes, chart-choice decision table, connector canon, shell escape, resident + batch, Delivery Gate 1–5a — is inherited, not re-taught. This file adds only what **Morph** needs on top: cross-slide shape-name binding, Scene Actors vs content prefixing, ghost discipline, `transition=morph` CLI quirks, 52-style visual library lookup, and a morph-specific fresh-eyes Gate 5b extension. -**Philosophy**: Trust yourself to learn through practice. This skill provides workflow and references — you bring creativity and judgment. +When the pptx base rules cover it, the text here says `→ see pptx v2 §X`. Read `skills/officecli-pptx/SKILL.md` first if you have not. ---- +## Setup -## Use when +If `officecli` is missing: -- User wants to generate a `.pptx` +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` ---- +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. -## What is Morph? +## ⚠️ Help-First Rule -PowerPoint's Morph transition creates smooth animations by matching shapes with **identical names** across adjacent slides. +**This skill teaches the Morph workflow — when shape names must match, when to ghost, when the CLI auto-prefixes — not every command flag.** When a prop name, enum, or preset is uncertain, consult help BEFORE guessing. -``` -Slide 1: shape name="!!circle" x=5cm width=8cm -Slide 2: shape name="!!circle" x=20cm width=12cm - ↓ -Result: Circle smoothly moves and grows +```bash +officecli help pptx slide # authoritative for: transition, advanceTime, advanceClick, background +officecli help pptx shape # name, preset, x/y/width/height, fill, rotation, opacity, animation +officecli help pptx animation # preset + trigger + duration values +officecli help pptx --json # machine-readable schema ``` -**Three core concepts:** +Help reflects the installed CLI version. When skill and help disagree, **help wins.** Every `--prop X=` in this file is grep-verified against `officecli help pptx `. Specific confirmations: `transition=morph` is a listed value on `slide`; `advanceTime` / `advanceClick` are valid. **There is NO standalone `transition` element** — `officecli help pptx transition` returns error. Sub-props such as `duration` / `delay` / `easing` for the transition itself are **not exposed on `slide`** — see §Known Issues for the raw-set path if you need them. -- **Scene Actors**: Persistent shapes with `!!` prefix that evolve across slides -- **Ghosting**: Move shapes to `x=36cm` (off-screen) instead of deleting -- **Content**: Text/data added fresh per slide, previous content ghosted first +## Mental Model & Inheritance -For details: `reference/pptx-design.md` +**Inherits pptx v2.** You should have read `skills/officecli-pptx/SKILL.md` first. This skill assumes you know how to: add slides + shapes + charts + connectors; address by `@name=` / `@id=`; quote paths; use `batch` heredocs; use `tailEnd=triangle` on flow connectors; run the Delivery Gate 1–5a; attribute `[AGENT-ERROR]` vs `[RENDERER-BUG]` vs `[SKILL gap]`. If any of those are unfamiliar, read pptx v2 first. ---- +**Inherited from pptx v2 (do NOT re-teach):** -## Workflow +- Visual delivery floor — title ≥ 36pt / body ≥ 18pt / title ≥ 2× body, cover-richness, contrast floor, no `\$\t\n` literals, ≤ 1 animation per slide / ≤ 600ms. +- Grid math — 33.87 × 19.05cm, edge margin ≥ 1.27cm, inter-block gap ≥ 0.76cm, ≥ 20% negative space. For N-card grids: `col = (33.87 − 2·margin − (N−1)·gap) / N`. +- Four canonical palettes (Executive navy / Forest & moss / Warm terracotta / Charcoal minimal) — morph decks may pick a different mood from `reference/styles/`, but contrast rules still apply. +- Chart-choice table — column vs bar vs line vs pie vs scatter vs large-text KPI; `> 3 series + > 8 categories` = split. +- Connector canon — `shape=straight|elbow|curve`, `@id=` for from/to (C-P-6), `tailEnd=triangle` on every flow. +- Shell escape 3-layer — `$` single-quoted, heredocs for batch, `` for real newlines. +- Resident mode + batch ≤ 12 ops, `<<'EOF'` single-quoted delimiter. +- Delivery Gate 1-5a (schema, token grep, hyperlink rPr, slide-order, dark-on-dark) — every gate prints OK before declaring done. +- Known Issues C-P-1..7 (hyperlink rPr, chart spPr warning, animation duration readback, animation remove, connector enum, connector `@name=`, chart color renderer normalization). +- Attribution triage — `[AGENT-ERROR]` vs `[RENDERER-BUG]` vs `[SKILL gap]`. -### Phase 1: Understand the Topic +**Morph identity — what this skill owns (delta on top of pptx v2):** -Ask only when topic is unclear, otherwise proceed directly. +- **Cross-slide shape-name binding.** PowerPoint's Morph engine pairs shapes by **identical `name=`** across adjacent slides and interpolates their position / size / rotation / fill / opacity. No matching name ⇒ no animation, silent fade. This is a workflow discipline, not a CLI feature. +- **Namespace prefixes:** `!!scene-*` (persistent decoration, never ghosted) / `!!actor-*` (content that evolves then exits) / `#sN-*` (per-slide content, ghosted on slide N+1). Plan the names BEFORE you `add`. +- **Ghost position `x=36cm`** (off the right edge of the 33.87cm canvas). Never delete a `!!`-prefixed shape — move it off-canvas so the morph exit animation still plays. +- **`transition=morph` auto-prefix quirk.** The CLI auto-prepends `!!` to every shape on a morph slide, which silently breaks `@name=` path selectors. Use `/slide[N]/shape[K]` index paths after morph is set. See §Known Issues. +- **Adjacent-slide spatial variety.** Displacement ≥ 5cm or rotation ≥ 15° between pairs — otherwise morph interpolates nothing visible. +- **Renderer reality.** Morph renders in PowerPoint 365 / Keynote / WPS. LibreOffice and many web viewers render as plain fade (runtime feature). Not a skill defect — `[RENDERER-BUG]`. ---- +### Reverse handoff — when to go BACK to pptx base (or sibling skills) -> **⚠️ CRITICAL KNOWN ISSUE: Name-based path selectors break after `transition=morph` is set** -> After calling `officecli set '/slide[N]' --prop transition=morph`, paths like `/slide[N]/!!my-shape` return 'Element not found'. The CLI auto-prepends `!!` to shape names when morph is applied, which invalidates name-based lookups. -> -> **Workaround:** Always use shape INDEX paths instead of name paths when accessing shapes on morph slides: -> ```bash -> # WRONG (after transition=morph set): -> officecli get deck.pptx '/slide[3]/!!my-circle' --depth 1 -> -> # CORRECT: -> officecli get deck.pptx '/slide[3]' --depth 1 # first list all shapes to find index -> officecli get deck.pptx '/slide[3]/shape[2]' --depth 1 -> ``` -> The build.py template should use `inspect()` + index-based access throughout. +Stay in **pptx v2 base** for any deck without cross-slide motion (board reviews, sales decks, all-hands, training). Stay in **officecli-pitch-deck** for fundraising narrative arcs without morph. Use this skill only when the user explicitly asks for "morph" / "smooth transitions" / "continuous animation" AND ≥ 2 consecutive slides share a visual element that transforms. "Animated deck" meaning one-off entrance animations → pptx v2 §Animations, not morph. ---- +## Shell & Execution Discipline -### Phase 2: Plan the Story +**Shell quoting, incremental execution, `$FILE` convention** → see pptx v2 §Shell & Execution Discipline. Same rules verbatim. -**FIRST: Read the thinking framework** +**Morph-specific additions:** -→ Open and read `reference/decision-rules.md` — it provides the structured approach for planning compelling presentations (Pyramid Principle, SCQA, page types). +- **`!!` in shell values — single-quote.** Bash / zsh history expansion eats unquoted `!!foo`. Always use `--prop 'name=!!scene-ring'` (single quotes). In Python `subprocess.run([...])` lists, no quoting needed — pass `"name=!!scene-ring"` as a plain string. +- **`$` in prop text — single-quote (price tokens).** `--prop text='$9/mo'` and `--prop text='$199/yr'` — NEVER `--prop text="$9/mo"` (zsh/bash eat `$9` as empty var → text rendered as `.` / stray period). Same for `${VAR}`, `$USER`, `\n`, `\r`, `\t` inside a double-quoted prop. Gate 2 morph addendum below greps for the leak signature. +- **`#` in shell values — safe, but quote anyway.** `#` is a comment leader only at the start of a shell word. `--prop name=#s1-title` works, but `--prop 'name=#s1-title'` is the habit that stops you guessing. +- **Batch heredoc is the cleanest path for multi-shape slides.** `<<'EOF' | officecli batch $FILE` disables all shell expansion — safe for `$`, `!!`, `#`, `'` inside the JSON body. +- **`--json` responses wrap the payload in `.data.*`.** `query` returns `.data.results[]` (array of matches); `get` returns `.data.children[]` (direct content); `format` always sits at `.data.results[].format.X` / `.data.children[].format.X`. Always prefix jq paths with `.data.` — bare `.children[]` or `.results[]` returns null silently. +- **Variable:** `FILE="deck.pptx"` at the top of every build script; every example below uses `$FILE`. +- **Gate shell pattern — COUNT, then if/else.** Never write `grep … && echo LEAK || echo OK` — when grep exits 1 (0 matches), the `||` branch fires with empty stdout and prints "OK" confusingly (or prints "LEAK" from prior pipes). Canonical form: `COUNT=$(cmd | wc -l); if [ "$COUNT" -gt 0 ]; then echo "LEAK: …"; else echo "OK"; fi`. -**Then create `brief.md`** with: +## Two primitives this skill owns -- **Context**: Topic, audience, purpose, narrative structure (SCQA or Problem-Solution) -- **Outline**: Conclusion first + slide-by-slide summary -- **Page briefs**: For each slide: - - Objective (what should this slide achieve?) - - Content (specific text/data to include) - - Page type (title | evidence | transition | conclusion) - - Design notes (visual emphasis, scene actor behavior) +- **Scene Actors** = persistent `!!`-named shapes (decoration or content) **paired by identical name** across adjacent slides so Morph can interpolate them. Every `!!scene-*` / `!!actor-*` shape is a scene actor. +- **Choreography** = the plan for how actors evolve — who moves where, who enters, who exits, on which slide pair. Written BEFORE code in the §Morph Pair Planning table. -**Morph Pair Scene Planning (REQUIRED before building)** +Use this skill when the user asks for morph motion AND ≥ 2 consecutive slides share a visual element that transforms. Target-viewer caveat: morph needs PowerPoint 365 / Keynote / WPS — if the user is LibreOffice-only, warn first (see §Renderer honesty). -For every morph transition, plan the slide pair BEFORE writing any code. Use a table like this in `brief.md`: +**Speaker notes rule.** Every content slide (non-cover, non-closing) MUST carry speaker notes via `officecli add "$FILE" /slide[N] --type notes --prop text='…'`. Missing notes = not shippable — inherits pptx v2 §Hard rules (H7). Morph decks tend to be visually minimal, so notes carry the narration. -| Pair | Slide A (start) | Slide B (end) | Visual narrative purpose | -|------|-----------------|---------------|--------------------------| -| 1→2 | Ring centered, title appears | Ring shifts right, subtitle revealed | Attention → context | -| 2→3 | Feature box large | Feature box small, metric card grows | Zoom out → detail | -| 3→4 | Metric card exits (ghost), new actor enters | Actor repositions | Section transition | +## What is Morph? (core mechanics) -**Rules for the planning table:** -- Determine ALL `!!` shape names during planning — the same name must be used identically across the slide pair -- For each `!!` shape, decide its role: `!!scene-{desc}` (background/decoration) or `!!actor-{desc}` (content/foreground) -- Mark which shapes need to be ghosted at each section transition -- Do NOT start building until the naming table is complete — renaming shapes mid-build causes ghost accumulation bugs +PowerPoint's Morph transition creates smooth motion by interpolating shape properties between adjacent slides, matched by **identical shape names**. ---- +``` +Slide 1: shape name="!!scene-ring" x=5cm width=8cm fill=E94560 opacity=0.3 +Slide 2: shape name="!!scene-ring" x=20cm width=12cm fill=E94560 opacity=0.6 + ↓ transition=morph on slide 2 +Result: Ring smoothly moves, grows, and fades darker over ~1 second +``` -### Phase 3: Design and Generate +Morph only runs if slide N+1 carries `transition=morph`. Apply it via `officecli add / --type slide --prop transition=morph` on creation, or `officecli set "/slide[N]" --prop transition=morph` after the fact. Slides 2+ that omit this prop fall back to whatever the master defines (usually no transition) — motion dies silently. -**Before generation starts, always remind the user:** +**Three-prefix naming system (non-negotiable):** -- The PPT file may be rewritten multiple times during build. -- Once the PPT file appears in the workspace, the user can preview the live generation progress directly in AionUi. -- Do **not** click "Open with system app" during generation, to avoid file lock / write conflicts. -- Use clear, direct language and make this a concrete warning, not an optional suggestion. +| Prefix | Role | Lifecycle | Example | +|---|---|---|---| +| `!!scene-*` | Background / decoration — persists across the entire deck | Set once, adjust position/size to create motion; **rarely ghosted** | `!!scene-ring`, `!!scene-bg-band`, `!!scene-grid` | +| `!!actor-*` | Content / foreground — evolves across a section | Introduced on slide N, modified on slide N+1, N+2…, **ghosted to `x=36cm`** on its exit slide | `!!actor-feature-box`, `!!actor-metric`, `!!actor-headline` | +| `#sN-*` | Per-slide content (titles, bullets, captions) | Added fresh on slide N, **ghosted to `x=36cm`** on slide N+1 | `#s1-title`, `#s2-kpi`, `#s3-caption` | -**FIRST: Ensure latest officecli version** +**Hard rule:** `!!scene-*` and `!!actor-*` names must NEVER collide (e.g., `!!scene-card` + `!!actor-card` in the same deck — morph engine confuses them). Disambiguate: `!!scene-card-bg` vs `!!actor-card-content`. -Follow the installation check in `reference/officecli-pptx-min.md` section 0 (checks version and upgrades only if needed). +**Charts are opaque to morph.** `officecli add … --type chart` does NOT accept `--prop name=!!…` (returns `UNSUPPORTED props: name`), so a chart cannot participate in shape-name morph pairing. For bar-grow / line-grow narratives: (a) accept plain fade-in of the chart as-is, OR (b) build N `!!actor-bar-K` rectangles manually sized to the values and morph those — each rect carries the same `!!actor-bar-K` name across adjacent slides while width / height / fill evolves. -**IMPORTANT: Use morph-helpers for reliable workflow** +**Ghost accumulation is silent.** Once a `!!`-prefixed shape appears on any slide, it stays visible on every subsequent morph slide unless explicitly moved to `x=36cm`. `final-check` helper does NOT detect `!!` shapes lingering in the visible area — **only Gate 5b screenshot audit does.** Plan every actor's exit slide in the pair table BEFORE coding. -Generate a Python script that uses `reference/morph-helpers.py` — this provides helper functions with built-in verification. Python works cross-platform (Mac / Windows / Linux). +**Spatial variety rule.** Adjacent slides must have **noticeably different** compositions — displacement ≥ 5cm OR rotation ≥ 15° OR size delta ≥ 30% on at least 3 morph-paired shapes. Without this, morph interpolates nothing visible and the transition collapses to a fade (silent-fail). -**Shape naming rules (for best results)**: +**Simultaneous-timing constraint.** All `!!` shapes in one morph pair animate simultaneously. To stagger shape A before shape B, insert an intermediate keyframe slide — there is no per-shape delay knob. -Use these naming patterns for clear code and reliable verification: +**Paired vs enter vs exit — three behaviors, one rule.** Same mechanism (shape-name match) produces three outcomes: -**Namespace prefixes for `!!` shapes — prevent scene collision:** +| Behavior | Source slide A | Target slide B | Who carries `!!`? | +|---|---|---|---| +| **Paired morph** (interpolate) | has `!!foo` | has `!!foo` | both slides, identical name | +| **Enter** (fade / morph-in) | — (no counterpart) | has `!!foo` | target only — new shape | +| **Exit via ghost** (slide off) | has `!!foo` at visible `x` | has `!!foo` at `x=36cm` | both — same name, B is off-canvas | -All persistent `!!` shapes MUST use one of these two prefixes to avoid morph engine confusion when multiple morph pairs share similar shape names: +**Outgoing content (not incoming) is what gets `!!`-prefixed + ghosted.** `!!actor-*` shapes silently "disappear" when you forget them — their name going missing on slide B reads as an unpaired exit (plain fade). Always explicit-ghost to `x=36cm` so the exit animation slides off the right edge visibly. One runnable example: -- `!!scene-{desc}` — Background / decoration shapes (e.g., `!!scene-ring`, `!!scene-bg-gradient`, `!!scene-grid-line`) - - These persist across the entire deck; move them for motion but rarely ghost them -- `!!actor-{desc}` — Content / foreground shapes (e.g., `!!actor-feature-box`, `!!actor-metric`, `!!actor-label`) - - These carry slide-specific content; ghost them at section boundaries +```bash +# Slide 2: actor is visible at x=5cm — Slide 3: same name, ghosted off-canvas → visible slide-off motion +officecli add "$FILE" "/slide[3]" --type shape --prop 'name=!!actor-metric' \ + --prop text="42%" --prop x=36cm --prop y=8cm --prop width=6cm --prop height=3cm +``` -**Rule: `!!scene-*` and `!!actor-*` names must NEVER be identical.** -Bad: `!!scene-card` and `!!actor-card` in the same deck — morph engine will confuse them. -Good: `!!scene-card-bg` and `!!actor-card-content` — unambiguous. +**Content (`#sN-*`) is added fresh per slide.** Because text changes every slide, Morph has no meaningful pairing to do on titles / body — it cross-fades them. This is why `#sN-*` get different names per slide (they are intentionally unpaired) and must be ghosted on slide N+1. Scene actors (`!!`) carry the continuity; content (`#`) carries the message. -1. **Scene actors** (persistent across slides): - - Format: `name=!!scene-{desc}` or `name=!!actor-{desc}` - - Examples: `name=!!scene-ring`, `name=!!scene-dot`, `name=!!actor-feature-box` - - Behavior: Modify position/size/color across slides — do NOT delete - - **Exit strategy — two trigger scenarios**: - 1. **Permanent exit** (shape no longer needed): Move it off-screen to `x=36cm`. - Morph will smoothly slide it out of view. - Example: `officecli set deck.pptx '/slide[N]/!!FeatureBox' --prop x=36cm --prop y=14cm` - To bring it back on a later slide, simply move it back to a visible position. - 2. **Scene transition exit** (entering a new topic section): When the presentation - moves into a new thematic section, ALL `!!` content shapes from the previous - section must also be ghosted to `x=36cm`. Only decoration actors that persist - throughout the entire deck (e.g., a background ring) should remain visible. - ```bash - # Entering new section: ghost all previous section's !! content shapes - # First, check what !! shapes are on the current slide - officecli get deck.pptx '/slide[N]' --depth 1 - # Then ghost each one - officecli set deck.pptx '/slide[N]/!!FeatureBox' --prop x=36cm - officecli set deck.pptx '/slide[N]/!!MetricCard' --prop x=36cm - officecli set deck.pptx '/slide[N]/!!ChannelLabel' --prop x=36cm - ``` - **Rule**: Each new section's first slide should be clean — only current-section - actors visible; no leftover shapes from the previous section. +## Morph Pair Planning (pre-code, REQUIRED) -2. **Content shapes** (unique per slide): - - Format: `name=#sN-description` - - Pattern: `#` + `s` + slide_number + `-` + description - - Examples: `name=#s1-title`, `name=#s2-card1`, `name=#s3-stats` - - Behavior: Ghost (x=36cm) when moving to next slide +Before planning morph pairs, if the deck's audience / purpose / narrative is underspecified, run the planning prompt in `reference/decision-rules.md` to emit a `brief.md` first — a morph arc without a narrative spine collapses into "slide with motion", not "story with motion". -**Ghost accumulation — critical behavior to understand:** +Plan every transition in a table inside `brief.md` **before** writing any `officecli add`. Renaming shapes mid-build is the #1 cause of ghost accumulation bugs. -> Once a `!!`-prefixed shape appears on any slide, it persists and remains visible on **every subsequent morph slide** unless explicitly moved off-screen. +| Pair | Slide A (start) | Slide B (end) | Actors in play | Ghost on Slide B | +|---|---|---|---|---| +| 1→2 | `!!scene-ring` centered 5cm, `#s1-title` visible | Ring shifts to x=20cm, grows 8→12cm; `#s2-subtitle` revealed | `!!scene-ring` evolves | `#s1-title` → x=36cm | +| 2→3 | `!!actor-feature-box` large (14cm wide) | Feature box small (6cm), `!!actor-metric` enters | `!!scene-ring`, `!!actor-feature-box`, `!!actor-metric` | `#s2-subtitle` → x=36cm | +| 3→4 | Content section A | Section B divider | — | `!!actor-feature-box` + `!!actor-metric` → x=36cm (section-exit); `#s3-*` → x=36cm | -This means: -- A `!!actor-feature-box` introduced on slide 3 will still be visible on slides 4, 5, 6, 7 ... unless you ghost it -- Ghost accumulation builds silently — visual clutter compounds across the deck -- The `morph_final_check` tool does NOT catch `!!` shapes that linger in the visible area; only screenshot verification can detect this +**Planning rules:** -**Ghost cleanup pattern** — when a `!!actor-*` shape is no longer needed, exit it explicitly: +1. Decide ALL `!!` names up front — each morph-paired shape must use the **exact same name** on both slides. +2. Classify every `!!` shape as `!!scene-*` or `!!actor-*`. Scene shapes persist; actors must have a planned exit slide. +3. **Section-transition boundary:** when moving into a new topic section, ghost ALL previous-section `!!actor-*` on the first slide of the new section. Only `!!scene-*` (whole-deck decoration) remains. +4. Do NOT start building until the table is complete. If the plan changes mid-build, redraw the table and re-verify affected slides. -```bash -# Pattern: after the last slide where !!actor-feature-box is needed, -# on the NEXT slide's setup, move it off-screen BEFORE adding new content -officecli set deck.pptx '/slide[N]/shape[X]' --prop x=36cm --prop y=10cm +## Morph Recipes (4 patterns) + +Four patterns cover ~95% of morph decks. `$FILE="deck.pptx"` throughout. Each block is self-contained and ≤ 20 lines. + +### (a) Single-element morph — size / position -# If the shape served a 2-slide story arc (slides 3→4), ghost it on slide 5: -helper("ghost", OUTPUT, 5, ) +**Visual outcome.** A hero title centered on slide 1 (size 48pt at y=8cm), then slide 2 shrinks it to 32pt and shifts it to the top-left corner (x=1.5cm, y=1cm) — letting fresh slide-2 content take center stage. One shape, clean motion, no actors. + +```bash +FILE="deck.pptx" +officecli create "$FILE"; officecli open "$FILE" + +# Slide 1 — hero +officecli add "$FILE" / --type slide --prop layout=blank --prop background=1E2761 +officecli add "$FILE" /slide[1] --type shape --prop 'name=!!actor-headline' \ + --prop text="The one idea" --prop x=4cm --prop y=8cm --prop width=26cm --prop height=3cm \ + --prop font=Georgia --prop size=48 --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=none + +# Slide 2 — headline shrinks + moves; new body takes stage +officecli add "$FILE" / --type slide --prop layout=blank --prop background=1E2761 --prop transition=morph +officecli add "$FILE" /slide[2] --type shape --prop 'name=!!actor-headline' \ + --prop text="The one idea" --prop x=1.5cm --prop y=1cm --prop width=12cm --prop height=1.5cm \ + --prop font=Georgia --prop size=24 --prop bold=true --prop color=FFFFFF --prop align=left --prop fill=none +officecli add "$FILE" /slide[2] --type shape --prop 'name=#s2-body' \ + --prop text="Here is the supporting evidence." --prop x=1.5cm --prop y=5cm --prop width=30cm --prop height=2cm \ + --prop font=Calibri --prop size=20 --prop color=CADCFC --prop fill=none + +officecli close "$FILE"; officecli validate "$FILE" ``` -**Rule**: For every `!!actor-*` shape, its "ghost slide" (where it exits) must be planned in the Phase 2 morph pair table. Do not leave any `!!actor-*` shape without a planned exit. +### (b) Multi-element coordinated morph — Actors / Choreography -**Why this naming matters:** +**Visual outcome.** Three scene actors (`!!scene-ring`, `!!scene-dot`, `!!scene-band`) repositioned across 3 slides to feel like a camera pan. Fresh per-slide titles fade in / out via the `#sN-*` ghost pattern. Use this when the narrative has a continuous visual backdrop. -- ✅ **Better detection**: Primary method (`#sN-` pattern matching) is fastest and most accurate -- ✅ **Readable code**: Anyone can tell `#s1-title` is slide 1's title -- ✅ **Easy debugging**: `grep "#s1-"` finds all slide 1 content quickly -- ⚠️ **Backup detection exists**: Even without `#` prefix, duplicate text detection will catch most issues (but has edge cases) +```bash +# Slide 1 — anchor composition (already built via recipe a; here we add actors) +officecli add "$FILE" /slide[1] --type shape --prop 'name=!!scene-ring' --prop preset=ellipse \ + --prop fill=E94560 --prop opacity=0.3 --prop x=5cm --prop y=3cm --prop width=8cm --prop height=8cm +officecli add "$FILE" /slide[1] --type shape --prop 'name=!!scene-dot' --prop preset=ellipse \ + --prop fill=0F3460 --prop x=28cm --prop y=15cm --prop width=1cm --prop height=1cm + +# Slide 2 — morph: ring moves + grows, dot slides left (spatial variety ≥ 5cm on both) +officecli set "$FILE" "/slide[2]" --prop transition=morph +officecli add "$FILE" /slide[2] --type shape --prop 'name=!!scene-ring' --prop preset=ellipse \ + --prop fill=E94560 --prop opacity=0.6 --prop x=20cm --prop y=2cm --prop width=12cm --prop height=12cm +officecli add "$FILE" /slide[2] --type shape --prop 'name=!!scene-dot' --prop preset=ellipse \ + --prop fill=0F3460 --prop x=3cm --prop y=16cm --prop width=1.5cm --prop height=1.5cm +# Ghost slide-1 content +officecli set "$FILE" "/slide[2]/shape[@name=#s1-title]" --prop x=36cm 2>/dev/null || true # name path may fail after morph — see Known Issues + +# Verify morph pair: identical names on slides 1 & 2 +officecli get "$FILE" /slide[1] --depth 1 --json | jq -r '.data.children[]?.format.name // empty' +officecli get "$FILE" /slide[2] --depth 1 --json | jq -r '.data.children[]?.format.name // empty' +# Compare — `!!scene-ring` and `!!scene-dot` MUST appear on both, byte-identical. +``` -**Bottom line**: Follow these patterns in your code examples, and verification will work smoothly. +### (c) Continuous multi-slide morph (story arc) — use helpers -**Then proceed with pattern**: +**Visual outcome.** A 5-slide arc telling one continuous story: same 2 scene actors drift across the canvas as the narrative progresses; content (`#sN-*`) refreshes per slide and is ghosted on the next. Building this by hand is ~60 commands — use `reference/morph-helpers.py` to keep the build script short and auto-verified. ```python #!/usr/bin/env python3 +# Invoke the provided helper library for clone + ghost + verify import subprocess, sys, os - -def run(*args): - result = subprocess.run(list(args)) - if result.returncode != 0: - sys.exit(result.returncode) - -# Load helper functions (provides morph_clone_slide, morph_ghost_content, morph_verify_slide) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +HELPERS = os.path.join(SCRIPT_DIR, "reference", "morph-helpers.py") +FILE = "deck.pptx" + def helper(*args): - run(sys.executable, os.path.join(SCRIPT_DIR, "reference", "morph-helpers.py"), *[str(a) for a in args]) - -OUTPUT = "deck.pptx" -run("officecli", "create", OUTPUT) -run("officecli", "open", OUTPUT) # Resident mode — all commands run in memory - -# ============ SLIDE 1 ============ -print("Building Slide 1...") -run("officecli", "add", OUTPUT, "/", "--type", "slide") -run("officecli", "set", OUTPUT, "/slide[1]", "--prop", "background=1A1A2E") - -# Scene actors (!!scene-* prefix = decoration, persists entire deck) -run("officecli", "add", OUTPUT, "/slide[1]", "--type", "shape", - "--prop", "name=!!scene-ring", "--prop", "preset=ellipse", "--prop", "fill=E94560", - "--prop", "opacity=0.3", "--prop", "x=5cm", "--prop", "y=3cm", "--prop", "width=8cm", "--prop", "height=8cm") -run("officecli", "add", OUTPUT, "/slide[1]", "--type", "shape", - "--prop", "name=!!scene-dot", "--prop", "preset=ellipse", "--prop", "fill=0F3460", - "--prop", "x=28cm", "--prop", "y=15cm", "--prop", "width=1cm", "--prop", "height=1cm") - -# Content shapes (#s1- prefix, will be ghosted on next slide) -# Use generous width (25-30cm for titles) to avoid text wrapping! -run("officecli", "add", OUTPUT, "/slide[1]", "--type", "shape", - "--prop", "name=#s1-title", "--prop", "text=Main Title", - "--prop", "font=Arial Black", "--prop", "size=64", "--prop", "bold=true", - "--prop", "color=FFFFFF", "--prop", "x=10cm", "--prop", "y=8cm", - "--prop", "width=28cm", "--prop", "height=3cm", "--prop", "fill=none") - -# ============ SLIDE 2 ============ -print("Building Slide 2...") - -# Use helper: automatically clone + set transition + list shapes + verify -helper("clone", OUTPUT, 1, 2) - -# Use helper: ghost all content from slide 1 (shape index 3 = #s1-title) -helper("ghost", OUTPUT, 2, 3) - -# Add new content for slide 2 -run("officecli", "add", OUTPUT, "/slide[2]", "--type", "shape", - "--prop", "name=#s2-title", "--prop", "text=Second Slide", - "--prop", "font=Arial Black", "--prop", "size=64", "--prop", "bold=true", - "--prop", "color=FFFFFF", "--prop", "x=10cm", "--prop", "y=8cm", - "--prop", "width=28cm", "--prop", "height=3cm", "--prop", "fill=none") - -# Adjust scene actors to create motion -# SPATIAL RULE: scene actors must stay in safe zones (see Shape naming rules above) -run("officecli", "set", OUTPUT, "/slide[2]/shape[1]", "--prop", "x=15cm", "--prop", "y=5cm") # !!scene-ring moves -run("officecli", "set", OUTPUT, "/slide[2]/shape[2]", "--prop", "x=5cm", "--prop", "y=10cm") # !!scene-dot moves - -# Use helper: verify slide is correct (transition + ghosting) -helper("verify", OUTPUT, 2) - -# ============ SLIDE 3 ============ -print("Building Slide 3...") - -# ============ SECTION TRANSITION: Ghost ALL !! content shapes from previous section ============ -# Before adding new section content, ghost every !! shape that belongs to the previous section. -# Run: officecli get deck.pptx '/slide[N]' --depth 1 to list all shapes and confirm indices. -# Then ghost each previous-section actor: -# helper("ghost", OUTPUT, N, shape_index_1) -# helper("ghost", OUTPUT, N, shape_index_2) -# ... repeat for ALL !! shapes that were part of the previous section -# VERIFY: After building, open screenshot of this slide and confirm zero overlap with previous section content. - -helper("clone", OUTPUT, 2, 3) -helper("ghost", OUTPUT, 3, 4) # Ghost #s2-title (now at index 4) - -run("officecli", "add", OUTPUT, "/slide[3]", "--type", "shape", - "--prop", "name=#s3-title", "--prop", "text=Third Slide", - "--prop", "font=Arial Black", "--prop", "size=64", "--prop", "bold=true", - "--prop", "color=FFFFFF", "--prop", "x=10cm", "--prop", "y=8cm", - "--prop", "width=28cm", "--prop", "height=3cm", "--prop", "fill=none") - -run("officecli", "set", OUTPUT, "/slide[3]/shape[1]", "--prop", "x=25cm", "--prop", "y=8cm") -run("officecli", "set", OUTPUT, "/slide[3]/shape[2]", "--prop", "x=10cm", "--prop", "y=5cm") - -helper("verify", OUTPUT, 3) - -# ============ FINAL VERIFICATION ============ -run("officecli", "close", OUTPUT) # Save from memory to disk -print() -print("=========================================") -helper("final-check", OUTPUT) - -print() -print("Build complete! Open", OUTPUT, "in PowerPoint to see morph animations.") + subprocess.run([sys.executable, HELPERS, *[str(a) for a in args]], check=True) + +# ... assume slide 1 is built with 2 scene actors (!!scene-ring, !!scene-dot) + #s1-title +# Helper builds slide 2–5 with: clone from previous + apply transition=morph + ghost previous #sN- content +for n in range(2, 6): + helper("clone", FILE, n - 1, n) # clone + set transition=morph + list shapes + helper("ghost", FILE, n, "all-content") # ghost all #s(n-1)-* via duplicate-text detection + # …then add THIS slide's #sN- content via officecli add as normal… +helper("final-check", FILE) # structural pass; DOES NOT catch !! lingering in visible area ``` -**Key advantages of using helpers:** +Helper signatures and source: `reference/morph-helpers.py` (`clone`, `ghost`, `verify`, `final-check`). The shell equivalent is `reference/morph-helpers.sh` — pick one per platform; do not mix. -- ✅ **Fewer steps**: `morph_clone_slide` = clone + transition + list + verify (4 steps → 1 function) -- ✅ **Instant feedback**: Each helper shows ✅ or ❌ immediately -- ✅ **Can't forget**: Transition and verification are automatic -- ✅ **Clear errors**: If something is wrong, you'll know exactly what and where -- ✅ **Dual detection**: Catches unghosted content by both naming pattern AND duplicate text detection - - Even if you forget `#` prefix, duplicate detection will still catch the problem! +**When to use helpers vs raw `officecli`.** For 2-3 slide decks, raw commands (recipes a, b) are clearer. For 5+ slides with repeating clone/ghost/verify cadence, helpers save ~40% of commands and provide built-in verification. Every slide is still closed by `officecli validate` before delivery. -**Scene Actor Spatial Rule (CRITICAL):** +### (d) Morph + fade hybrid — entrance on morph slide -Scene actors must stay in **safe zones** at all times — corners and edges only. -**DO NOT** let scene actors pass through or rest in the content area (`x=2~28cm, y=3~16cm`). +**Visual outcome.** A morph pair where `!!scene-ring` moves continuously while a NEW per-slide card fades in simultaneously. Used when a morph-paired backdrop carries the eye and fresh foreground content needs a softer entrance than a raw appearance. -``` -Safe zones: - Top-right corner: x ≥ 24cm, y ≤ 6cm - Bottom-right: x ≥ 24cm, y ≥ 12cm - Bottom-left: x ≤ 2cm, y ≥ 12cm - Off-screen (right): x ≥ 32cm (fully out of view — use for ghost position) +```bash +# Slide 2 already has transition=morph and !!scene-ring. Add a new card with fade-entrance. +officecli add "$FILE" /slide[2] --type shape --prop 'name=#s2-card' --prop preset=roundRect \ + --prop fill=F5F7FA --prop line=none --prop x=2cm --prop y=12cm --prop width=10cm --prop height=5cm + +# Apply simultaneous-with-morph fade entrance to the new card. +# 'fade-entrance-300-with' = fade in, 300ms, trigger=withPrevious (plays with the morph transition). +officecli set "$FILE" "/slide[2]/shape[@name=#s2-card]" --prop animation=fade-entrance-300-with +officecli get "$FILE" "/slide[2]/shape[@name=#s2-card]" --json | jq '.data.format.animation' # readback sanity ``` -Before planning any scene actor path, inspect existing shape coordinates: +**Why this works.** Morph animates the `!!scene-*` shapes only (they have a pair on slide 1); the new `#s2-card` has no slide-1 counterpart, so morph would default-fade it — `fade-entrance-300-with` makes that fade explicit and timed. Keep the animation per pptx v2 floor: ≤ 600ms, no bounce / swivel / fly-from-edge (`officecli help pptx animation` for the canonical preset list). -```bash -# List all shapes on a slide (check for coordinate conflicts before placing actors) -officecli get deck.pptx '/slide[N]' --depth 1 --json -``` +## Choreography — animation types + staggered timing -Confirm the actor's target position does **not** overlap any content shape's bounding box -(`x` to `x+width`, `y` to `y+height`). - -**Essential rules:** - -- **Naming**: Scene actors use `!!` prefix, content uses `#sN-` prefix (best practice for verification and readability) -- **Transition**: Every slide after the first MUST have `transition=morph` (without this, no animation!) -- **Ghosting**: Before adding new slide content, ghost ALL previous content shapes to `x=36cm` (don't delete) -- **Motion**: Adjust scene actor (`!!-*`) positions between slides for animation -- **Variety**: Create spatial variety between adjacent slides -- **Text Width**: Use generous widths to prevent text wrapping: - - Centered titles (64-72pt): **28-30cm width** - - Centered subtitles (28-40pt): **25-28cm width** - - Left-aligned titles: **20-25cm width** - - Body text: 8-12cm (single-column), 16-18cm (double-column) - - **When in doubt, make it wider!** See `reference/pptx-design.md` for details -- **Text size rule — 16pt minimum scope**: - The 16pt minimum applies to ALL text that conveys primary content. Exceptions allowed for: chart axis labels (≤12pt OK), section eyebrow/kicker labels (≤14pt OK if ≤5 words), decoration shapes with no narrative content. Each exception must be intentional — descriptive body text at 13pt is NOT exempt. - -**Choreography — timing and motion principles:** - -Understanding how morph animates multiple shapes helps you plan intentional motion: - -| Animation type | How to achieve it | -|----------------|-------------------| -| Simple move | Same shape on slide A and B, same size, different `x`/`y` — morph interpolates position | -| Scale transform | Same shape on slide A and B, different `width`/`height` — morph interpolates size and position | -| Move + scale | Different `x`, `y`, `width`, `height` simultaneously — morph handles all dimensions at once | -| Color shift | Same shape, different `fill` color — morph cross-fades the fill | -| Enter (fade in) | Shape exists only on slide B (no counterpart on slide A) — morph fades it in | -| Exit (fade out) | Shape only on slide A (no counterpart on slide B) — morph fades it out | +How morph animates multiple shapes determines what the audience sees. Pick the right mechanism for each pair: -**Multi-shape timing rule:** -- All `!!` shapes in the same morph pair animate **simultaneously** — there is no way to stagger their start times within a single pair -- If you need shape A to move before shape B, you MUST split the transition into two morph pairs (i.e., add an intermediate slide between them) +| Animation type | How to achieve it (between Slide A and Slide B) | +|---|---| +| Simple move | Same `!!` name on both slides, same size, different `x`/`y` — morph interpolates position | +| Scale transform | Same name, different `width`/`height` — morph interpolates size (and re-positions the center) | +| Move + scale | Different `x`, `y`, `width`, `height` simultaneously — morph handles all dimensions at once | +| Color / opacity shift | Same name, different `fill` or `opacity` — morph cross-fades the fill | +| Rotation | Same name, different `rotation` (degrees) — morph rotates along the shortest arc | +| Font size change | Same name, different `size` (pt) on text shape — interpolates in PowerPoint 365; less reliable on Keynote / WPS / LibreOffice (may degrade to crossfade). For portable motion, pair `size` change with a matching `width`/`height` delta or an `x`/`y` displacement — the spatial change keeps motion visible when size interpolation drops out | +| Enter (fade in) | Shape exists only on Slide B (no counterpart on A) — morph fades it in | +| Exit (fade out) | Shape exists only on Slide A (no counterpart on B) — morph fades it out | -**Staggered timing pattern** (two shapes, offset timing): +**Multi-shape timing constraint.** All `!!` shapes in one morph pair animate **simultaneously** — there is no per-shape delay / duration knob in the CLI (help confirms: no `morph.duration` / `morph.delay` on slide). To stagger shape A before shape B, **split the transition into two pairs** with an intermediate slide: ``` Slide 2 → Slide 3: !!actor-A moves (!!actor-B stays put) -Slide 3 → Slide 4: !!actor-B moves (!!actor-A stays put or has already exited) +Slide 3 → Slide 4: !!actor-B moves (!!actor-A stays put or ghosts) ``` -This requires slide 3 as an explicit intermediate keyframe — never try to fake staggering within a single morph pair. - -**Known CLI behaviors:** - -- **`!!` prefix auto-added after `transition=morph`**: After running `set --prop transition=morph` - on a slide, the CLI automatically prepends `!!` to all shape names on that slide - (e.g., `#s1-title` → `!!#s1-title`). This is expected behavior. - `morph-helpers.py` handles this correctly — its verification logic uses substring matching - and is not affected. - - > **⚠️ CRITICAL: Name-based path selectors break after `transition=morph` is set** - > After calling `officecli set '/slide[N]' --prop transition=morph`, paths like `/slide[N]/!!my-shape` return 'Element not found'. The CLI auto-prepends `!!` to shape names when morph is applied, which invalidates name-based lookups. - > - > **Workaround:** Always use shape INDEX paths instead of name paths when accessing shapes on morph slides: - > - > ```bash - > # WRONG (after transition=morph set): - > officecli get deck.pptx '/slide[3]/!!my-circle' --depth 1 - > - > # CORRECT: - > officecli get deck.pptx '/slide[3]' --depth 1 # first list all shapes to find index - > officecli get deck.pptx '/slide[3]/shape[2]' --depth 1 - > ``` - > - > The build.py template should use `inspect()` + index-based access throughout. - > - > **Pattern recommendation**: Pre-plan all shape indices in a comment block at the top of your build script before setting morph. This prevents index tracking errors as the slide's shape count grows. - -- **Shape index tracking**: After each batch of shape additions, run - `officecli get deck.pptx '/slide[N]' --depth 1` to confirm the current slide's - shape list and indices. This prevents off-by-one errors when manually computing - index values for subsequent ghost/set operations. - -**Design resources:** - -- `reference/pptx-design.md` — Design principles (Canvas, Fonts, Colors, Scene Actors, Page Types, Style References) -- `reference/officecli-pptx-min.md` — Command syntax -- `reference/styles//` — Visual style examples (optional inspiration, browse by use case in `styles/INDEX.md`) +Slide 3 is an explicit intermediate keyframe. Do NOT attempt to fake staggering via timing props on the shape's `animation=` prop — Morph runs before per-shape animations. ---- +**Good-enough variety heuristic (Best Practice — creative flexibility).** For a morph to read as "motion", change at least 3 of {x, y, width, height, rotation, fill, opacity} on the dominant paired shape, with displacement ≥ 5cm OR rotation ≥ 15° OR size delta ≥ 30%. One shape × 3 props is a valid creative pattern (focus on one hero element). + +**Delivery Gate 5b-morph-2 is stricter.** The gate hard-asserts ≥ 3 DIFFERENT `!!`-prefixed shapes each vary by ≥ 1 of {x, y, width, height, rotation, font-size} across the pair — integrity check for "is this really a morph or a pretend-morph". Heuristic informs creative intent; Gate decides delivery. **Brand-constant scenery (pinned header strip, footer bar, logo badge) does NOT count toward the 3-shape quota** — these are supposed to stay put; motion must come from 3 other named shapes. When in doubt, satisfy the stricter Gate. -### Phase 4: Visual Verification + Deliver +**Deck-length rhythm.** Filling every transition with morph reads as anxious, not cinematic. Pace morph moments to deck length: +- **8-10 slides (dense):** 3-5 morph moments; motion can cluster. +- **12-18 slides (ceremonial):** 3-5 TOTAL morphs, spaced every 4-6 slides; use `transition=morph` at section dividers so the animation reads as chapter punctuation, not continuous agitation. +- **18+ slides (Act-based):** structure into 3 acts with 1 long section-divider morph between acts (5-10s of deliberate motion with a brief hold), plus 2-3 quieter morphs inside each act. Lean heavier on `!!scene-*` continuity than per-slide `!!actor-*` churn. -## Phase 4 视觉验证(REQUIRED — final-check 通过后不可跳过) +## Scene-actor spatial rule -### 4A. morph_final_check.py(CLI 数量验证) +Scene actors and actors moving across the canvas MUST stay in predictable zones during morph — otherwise they cross over content and read as clutter. -If you used `morph-helpers.py`, the build script calls `helper("verify", ...)` and `helper("final-check", ...)` automatically. Also validate the final structure: +**Safe zones (prefer for scene actor rest positions and morph paths):** + +``` +Top-right corner: x ≥ 24cm, y ≤ 6cm +Bottom-right: x ≥ 24cm, y ≥ 12cm +Bottom-left: x ≤ 2cm, y ≥ 12cm +Off-canvas (ghost): x ≥ 33.87cm (canvas right edge; use x=36cm for explicit ghost) +``` + +**Avoid resting actors in the content core:** `x = 2~28cm, y = 3~16cm`. Actors may **pass through** the core during morph (that's the motion), but they should not end a slide parked there with high opacity unless they are content themselves (`!!actor-*` carrying the slide's message). + +**Before placing any scene actor, inspect existing shape bounds:** ```bash -officecli validate .pptx -officecli view .pptx outline +officecli get "$FILE" "/slide[$N]" --depth 1 --json | \ + jq -r '.data.children[]? | "\(.format.name // .path) x=\(.format.x) y=\(.format.y) w=\(.format.width) h=\(.format.height)"' ``` -### 4B. 截图目视验证(必须执行) +Confirm the actor's target position does not overlap any `#sN-*` content shape's bounding box (`x` to `x + width`, `y` to `y + height`). If it would overlap, lower actor `opacity` ≤ 0.15 OR move it to a safe zone. -**final-check 通过不等于视觉正确。** `morph_final_check` 只验证 `#sN-` 前缀 shapes 的 ghost 状态(x=36cm 检查),它**无法检测**: -- `!!` shapes 在场景切换后仍停留在可视区域(x < 33.87cm)——这类问题会通过 final-check 但产生视觉叠加 -- 相邻幻灯片间 scene actor 位置/尺寸未发生变化(动画静止) +## Style library lookup workflow -必须对每张 slide 截图验证: +`reference/styles/` holds 52 visual style directories (dark / light / warm / vivid / bw / mixed moods) — design inspiration, not templates. Use the library as **on-demand reference**, not as a content dump. -```bash -# 方案1: officecli view(pptx 有 SVG 预览) -officecli view deck.pptx svg --output-dir screenshots/ +**Why lookup, not copy.** Each of the 52 `build.sh` files is a complete style demo — but the coordinates were hand-tuned for that specific demo's content length. Copying them verbatim into a deck with different content produces overlaps and misalignment (flagged in `INDEX.md` L5-11). The library's value is the **design logic**: palette choice for a mood, signature shape, choreography pattern. Apply that logic to your own grid math. + +**Four-step lookup:** + +1. **Browse INDEX.** `reference/styles/INDEX.md` groups all 52 styles by palette category and mood (e.g. `dark--premium-navy` = authoritative / refined; `warm--earth-organic` = organic / grounded). The Quick Lookup table also shows each style's **primary hex trio** (bg / fg / accent) — if the user specified a brand color, scan the hex column to find the nearest match without opening every `style.md`. Pick 1 style that matches the topic mood OR aligns with the user-specified hex. +2. **Read philosophy.** Open `reference/styles//style.md` for design intent — type pairing, color logic, signature elements. +3. **Glance technique.** Open `reference/styles//build.sh` ONLY for technique reference (signature shapes, palette hex codes, choreography ideas) — **coordinates are known-buggy per `INDEX.md` L5-11**; do not copy them. +4. **Apply on your own canvas.** Build your deck using pptx v2 grid math + visual floor; borrow only the palette and the signature gesture. -# 方案2: LibreOffice PDF → Chrome PNG(更准确) -libreoffice --headless --convert-to pdf deck.pptx -# 然后用 Chrome DevTools MCP 截图每页 +**Pointer:** `→ see reference/styles//` — never inline-copy coordinates from a style build.sh. + +## Delivery Gate (inherits pptx v2 + morph additions) + +**Gate 1–5a: full port from pptx v2.** → see pptx v2 §Delivery Gate. Schema (whitelisting C-P-2 chart spPr), token grep (`$…$` / `{{…}}` / `\$\t\n` / `()` / `[]`), hyperlink rPr (C-P-1), slide-order sanity, dark-on-dark contrast (Gate 5a). **Refuse to declare done until every pptx Gate 1–5a prints its OK message.** Morph decks have the same token / schema / order risks as any pptx. + +### Gate 2 morph addendum — price / metric tokens eaten by zsh + +Pptx v2 Gate 2 covers `$…$`, `{{…}}`, `\$\t\n` literals, empty `()` / `[]`. Morph decks add a class of leaks: price / metric tokens (`$9/mo`, `$29/month`, `$199/yr`) written in double-quoted `--prop text="…"` — the shell eats `$9` as an empty variable and the CLI stores `/mo` or a stray period. Run this in addition to pptx Gate 2: + +```bash +# Gate 2 morph — price / metric token leaks + stray-period placeholders +# Pattern hits: bare prices ($9, $29, $9.99), /unit suffix ($9/mo, $199/yr), ${VAR}, \n/\r/\t, lone period +LEAKS=$(officecli view "$FILE" text | grep -nE '\$[0-9]+(\.[0-9]+)?(/(mo|month|yr|year|day|wk|week|hr|hour))?|\$\{[A-Z_]+\}|\\[nrt]|^\.$' || true) +if [ -z "$LEAKS" ]; then echo "Gate 2 morph OK"; else echo "LEAK: $LEAKS"; fi ``` -逐 slide 检查清单: -- [ ] 每张 slide 中,前一节的 `!!` content shapes 均不可见(x >= 33.87cm 已移出视野) -- [ ] 每个场景切换的第一张 slide(新章节起始):前一节所有 `!!` shapes 已 ghost -- [ ] 最后一个场景的收尾 slide:整洁,无残留前场景内容 -- [ ] 装饰性 `!!` shapes(背景圆、角标等)在正确位置 +Covers: `$9` `$9.99` `$29/month` `$199/yr` `$1/day` `${VAR}` `\n`/`\r`/`\t` literals + stray `.` placeholders. Fix: single-quote the prop (`--prop text='$9/mo'`). + +### Gate 5b — Visual audit via HTML preview (MANDATORY) — extended for morph + +Run `officecli view "$FILE" html` and Read the returned HTML path. For every slide, answer the pptx v2 Gate 5b questions (overlap / dark-on-dark / divider overlap / order sanity / missing arrowheads) PLUS these four morph-specific checks: + +**Important: selectors with prefix match.** `officecli query` only supports operators `=`, `!=`, `~=`, `>=`, `<=`, `>`, `<` — there is NO `^=` prefix operator. A selector like `shape[name^=!!actor-]` returns an `invalid_selector` error. For "starts-with" filtering, use a `get --depth 1` loop + `jq startswith()` as shown below. + +- **5b-morph-1 — `!!actor-*` leak into visible area after its section ends.** For every `!!actor-*` that should have exited, confirm `x ≥ 33.87cm` (canvas right edge). Loop + filter (selector-safe): + ```bash + NSLIDES=$(officecli query "$FILE" slide --json | jq '.data.results | length') + for N in $(seq 1 $NSLIDES); do + officecli get "$FILE" "/slide[$N]" --depth 1 --json | \ + jq -r --arg n "$N" '.data.children[]? | + select(.format.name? // "" | startswith("!!actor-")) | + select((.format.x // "0cm" | rtrimstr("cm") | tonumber) < 33.87) | + "slide \($n) leak: \(.format.name) stuck at x=\(.format.x)"' + done + ``` + Any line printed = actor stuck visible. `final-check` misses this — only the loop + Read HTML do. + +- **5b-morph-2 — Adjacent slides have identical spatial composition (no motion).** Hard rule: between every morph pair, ≥ 3 DIFFERENT `!!`-prefixed shapes must each differ by ≥ 1 of {x, y, width, height, rotation, font-size}. Proof loop (dump both slides, diff same-name shapes, count differing shapes): + ```bash + for K in 1 2 3 4; do + A=$(officecli get "$FILE" "/slide[$K]" --depth 1 --json | \ + jq -r '.data.children[]? | select(.format.name? // "" | startswith("!!")) | + "\(.format.name)|\(.format.x)|\(.format.y)|\(.format.width)|\(.format.height)|\(.format.rotation // 0)"') + B=$(officecli get "$FILE" "/slide[$((K+1))]" --depth 1 --json | \ + jq -r '.data.children[]? | select(.format.name? // "" | startswith("!!")) | + "\(.format.name)|\(.format.x)|\(.format.y)|\(.format.width)|\(.format.height)|\(.format.rotation // 0)"') + VARIES=$(diff <(echo "$A") <(echo "$B") | grep -c '^[<>]') + if [ "$VARIES" -lt 6 ]; then echo "pair $K→$((K+1)) FLAT: only $VARIES diff-lines (need ≥ 6 = 3 shapes × 2 sides)"; fi + done + ``` + +- **5b-morph-3 — Morph-pair name mismatches.** Adjacent slides must share at least 2 `!!`-prefixed names exactly. Proof (note: `.data.children[]` — bare `.children[]` returns null): + ```bash + for N in 1 2 3 4 5; do + echo "--- slide $N ---" + officecli get "$FILE" "/slide[$N]" --depth 1 --json | \ + jq -r '.data.children[]? | select(.format.name? // "" | startswith("!!")) | .format.name' + done + ``` + Visually compare sequential blocks — shared `!!` names between N and N+1 are the morph pairs. Zero overlap = the pair is a plain fade. + +- **5b-morph-4 — `#sN-*` lingering on slide N+1 (ghost leak).** Per-slide content MUST be ghosted (`x=36cm`) on the NEXT slide. Loop + filter per N≥2: + ```bash + NSLIDES=$(officecli query "$FILE" slide --json | jq '.data.results | length') + for N in $(seq 2 $NSLIDES); do + PREV=$((N-1)) + officecli get "$FILE" "/slide[$N]" --depth 1 --json | \ + jq -r --arg n "$N" --arg p "$PREV" '.data.children[]? | + select(.format.name? // "" | startswith("#s\($p)-")) | + select((.format.x // "0cm" | rtrimstr("cm") | tonumber) < 33.87) | + "slide \($n) leak: \(.format.name) stuck at x=\(.format.x)"' + done + ``` + Any line printed = a `#s(N-1)-*` shape stayed visible on slide N. Ghost it. + +**REJECT the delivery** if any 5b-morph-1..4 loop prints a line. Collect stdout from all four loops into one stream and enforce with the COUNT pattern: `LEAK_COUNT=$(...all four loops... | wc -l); if [ "$LEAK_COUNT" -gt 0 ]; then echo "REJECT: $LEAK_COUNT morph leaks"; else echo "Gate 5b-morph OK"; fi`. + +## Renderer honesty + +**Morph renders in:** PowerPoint 365 (Windows/Mac), Keynote, WPS, PowerPoint Online. + +**Morph does NOT render in:** LibreOffice Impress (renders static, sometimes as fade), Google Slides web viewer (loses interpolation), most HTML / SVG viewers, `officecli view html` (structural only — morph is runtime). This is `[RENDERER-BUG]`, not a skill defect. Tell the user explicitly: "Open in PowerPoint 365 / Keynote / WPS to see the morph motion; other viewers will show static or plain fade." + +Static screenshots from any renderer **cannot verify morph motion** (the motion only exists at runtime). Use Gate 5b queries above to prove pair correctness; use a live viewer to prove motion quality. + +## Ghost Discipline & Actor Lifecycle + +**Every `!!actor-*` and `#sN-*` shape must be managed across EVERY slide, not just its "exit" slide.** + +### The Per-Slide Ghosting Rule + +When building a multi-slide morph deck: +1. **Slide N: Introduce `!!actor-ring` (visible at x=0cm)** +2. **Slide N+1: Add new content. Before finishing, ghost `!!actor-ring` to `x=36cm`.** +3. **Slide N+2: Add more content. Re-ghost `!!actor-ring` to `x=36cm` again.** (Not optional — even though it was already off-screen, each slide is a fresh canvas.) +4. **Slide N+3: If `!!actor-ring` should be visible again, move it back to x=0cm or its new position.** + +**Why:** Each slide's shape list is independent. Moving a shape off-canvas on slide N does NOT carry over to slide N+1 — if you forget to re-ghost it, it will re-appear at its original position on N+1. + +### Workflow Pattern (Bash) -**If verification fails**, see Troubleshooting section below. +```bash +# After adding new content shapes to slide $SLIDE: +for ACTOR in "!!actor-ring" "!!actor-dot" "!!actor-accent-bar"; do + officecli set "$FILE" "/slide[$SLIDE]/shape[@name=$ACTOR]" --prop x=36cm || true +done +``` ---- +Or in a build loop: -**Outputs** (3 files): +```bash +for SLIDE_NUM in 3 4 5 6 7 8 9 10 11; do + # Add content specific to this slide + officecli add "$FILE" "/slide[$SLIDE_NUM]" --type shape ... + + # IMMEDIATELY ghost all old actors (M-2 prevention) + officecli set "$FILE" "/slide[$SLIDE_NUM]/shape[@name=!!actor-ring]" --prop x=36cm || true + officecli set "$FILE" "/slide[$SLIDE_NUM]/shape[@name=!!actor-dot]" --prop x=36cm || true +done +``` -1. `.pptx` -2. Build script (complete, re-runnable — bash/python/powershell/etc.) -3. `brief.md` — **MUST be a standalone file** (not embedded inside test-report.md or any other file). - Content: slide-by-slide plan, content per slide, morph design decisions, ghost strategy per transition. +### Detection: Ghost Count Gate -**Final delivery message requirements:** +`morph-helpers.py final-check` counts all shapes at `x ≥ 34cm`. If count > 50, it prints: +``` +REJECT: Found 135 accumulated ghosts — likely M-2 ghost accumulation. +Run: officecli query deck.pptx 'shape[x>=34cm]' --json | jq '.data.results | length' +Expected ≤ 50 (roughly 4–5 active actors × 10–12 slides). +``` -- Tell the user the deck with polished Morph animations is ready. -- Explicitly recommend opening the generated PPT now to preview the motion effects. -- Use affirmative wording (e.g., "ready now", "open it now to preview the animation quality"). +**Fix:** Review the build log, ensure every slide re-ghosts all actors that should not appear in it. Re-run final-check. If still > 50, use `morph-helpers.py clean-accumulation deck.pptx` (see reference section). ---- +## Common Morph Pitfalls (design + workflow traps) -### Troubleshooting +Base pptx pitfalls (shell quoting, zsh `[N]` globbing, hex `#` prefix, `\n` in prop text) → see pptx v2 §Common Pitfalls. These are the morph-specific traps: -**If `morph_verify_slide` or `morph_final_check` reports issues:** +| Pitfall | Correct approach | +|---|---| +| `!!scene-card` and `!!actor-card` in the same deck | Names must be unique across prefixes. Rename: `!!scene-card-bg` vs `!!actor-card-content` | +| Renaming shapes mid-build after some slides are already done | Ghost accumulation bug waiting to happen. Stop, redraw the §Morph Pair Planning table, rerun affected slides | +| Placing `!!actor-*` into the content core without planning an exit | Every `!!actor-*` needs a ghost slide. Plan it in the pair table BEFORE coding | +| **Ghost accumulation (M-2): forgetting to re-ghost `!!actor-*` on later slides** | **CRITICAL:** When you add new content to slide N+1, ALL `!!actor-*` from slide N that should not be visible must be moved to `x=36cm` again. Do NOT assume they stay off-screen once ghosted — each slide is independent. Build pattern: `for each new slide: add content shapes → then loop: set each active !!actor-* to x=36cm`. `morph-helpers.py final-check` will REJECT if ghost count exceeds 50. | +| Forgetting `transition=morph` on a slide | Silent fade. Gate 5b-morph-2 (no motion) catches it; fix via `set /slide[N] --prop transition=morph` | +| Using `@name=` path on a morph slide after `transition=morph` was set | Selector breaks (M-1). Switch to index paths `/slide[N]/shape[K]` | +| Adjacent slides visually identical | Morph has nothing to interpolate — collapses to plain fade. Apply §Scene-actor spatial rule and move ≥ 3 shapes by ≥ 5cm / ≥ 15° | +| Trying to stagger 2 shapes via per-shape timing | Not supported — split the pair into two transitions with an intermediate keyframe slide | +| Testing morph motion in LibreOffice or a browser | `[RENDERER-BUG]`, not skill defect. Test in PowerPoint 365 / Keynote / WPS | +| Deleting a `!!` shape on exit instead of ghosting it | Deletion breaks morph pairing — the shape vanishes without animation. Always ghost to `x=36cm` | +| Writing `--prop text="$9/mo"` with double quotes | Shell eats `$9` as empty variable → text stored as `/mo` or stray `.`. Use single quotes: `--prop text='$9/mo'`. Gate 2 morph addendum greps this leak. | +| Using `` literal inside `--prop text='line1line2'` | Stored as 7 literal characters, not a line break. Use `officecli add "/slide[N]/shape[@id=K]" --type paragraph` once per line (M-6). | +| Using `shape[name^=!!actor-]` selector | `officecli query` has no `^=` operator — returns `invalid_selector`. Use `get /slide[N] --depth 1 --json \| jq '.data.children[]? \| select(.format.name \| startswith("!!actor-"))'`. | +| Running `validate` while resident mode is open | Pptx v2 inherits this trap — `officecli close "$FILE"` BEFORE `validate` | -1. **Missing transition**: +## Known Issues & Pitfalls - ```bash - # Check which slides are missing transition - officecli get .pptx '/slide[2]' --json | grep transition - officecli get .pptx '/slide[3]' --json | grep transition - # Expected: "transition": "morph" +Base pptx bugs C-P-1..7 (hyperlink rPr, chart ChartShapeProperties warning, animation duration readback, animation remove, connector enum, connector `@name=`, chart-color renderer normalization) all apply. **→ see pptx v2 §Known Issues C-P-1..7 for workarounds.** - # Fix: - officecli set .pptx '/slide[2]' --prop transition=morph - ``` +**Morph-specific (M-1..5):** -2. **Unghosted content**: +| # | Symptom | Workaround | +|---|---|---| +| **M-1** | After `officecli set '/slide[N]' --prop transition=morph`, every shape on that slide has `!!` auto-prepended to its name (`#s1-title` → `!!#s1-title`). Name-path selectors like `/slide[N]/shape[@name=#s1-title]` stop matching silently. **Selector filter caveat:** after auto-prefix, `!!#sN-caption` coexists alongside `!!actor-*` — filtering "scene actors" with `startswith("!!")` produces false matches on auto-prefixed content. Always filter with `startswith("!!actor-")` or `startswith("!!scene-")`, never bare `startswith("!!")`. | Use **index paths** after morph is set: `get /slide[N] --depth 1` to list shapes, then address via `/slide[N]/shape[K]`. Keep a shape-index comment at the top of the build script. | +| **M-2 🚨** | **Ghost accumulation — `!!actor-*` introduced on slide 3 stays visible on slides 4, 5, 6 unless EXPLICITLY ghosted every page.** `final-check` helper detects this and rejects if ghost count > 50. | **MANDATORY per-slide rule:** After you add new content to a slide, immediately set ALL active `!!actor-*` from previous slides to `x=36cm` (or explicitly position them visible if they belong in the current context). Example: `officecli set /slide[4]/shape[@name=!!actor-ring] --prop x=36cm`. Run after EVERY slide addition, not just at the end. See §Ghost Discipline & Actor Lifecycle below. | +| **M-3** | Section-transition boundary — on the first slide of a new topic section, previous-section `!!actor-*` shapes visibly linger. No command errors; only visual clutter. | On every section-start slide, explicitly ghost ALL `!!actor-*` from the previous section to `x=36cm`. Scene shapes (`!!scene-*`) stay. | +| **M-4** | `officecli help pptx slide` lists `transition=` but NO sub-props for duration / delay / easing of the transition itself. Agents sometimes invent `morph.duration=` / `transition.delay=` — they are rejected as UNSUPPORTED. | Accept defaults (morph ~1s, linear ease). For custom speed, use `raw-set` to add the `spd` attribute on `` — see M-4 example block below. Help does not list sub-props; `raw-set` is the only path. | +| **M-5** | `[RENDERER-BUG]` LibreOffice / Google Slides web viewer render morph slides as plain fade (no interpolation). | Test in PowerPoint 365 / Keynote / WPS. Not a skill defect — do not chase. | +| **M-6** | `` written inside `--prop text='line1line2'` is stored as the literal 7-character string, NOT interpreted as a line break. Audience sees `line1line2` rendered verbatim. | For multi-line bullets / captions, add one paragraph per line: `officecli add "/slide[N]/shape[@id=K]" --type paragraph --prop text='line1'` then repeat with `text='line2'`. See pptx v2 §Shell escape for the real-newline workflow. | - ```python - # Find unghosted shapes manually - import subprocess - for slide in range(2, 7): - print(f"Slide {slide}:") - subprocess.run(["officecli", "get", ".pptx", f"/slide[{slide}]", "--depth", "1"]) - # If you see shapes like "#s1-title" on slide 2 (not at x=36cm), they should be ghosted +**M-4 example — slow down all morph transitions** (`raw-set` requires a `` positional arg; `//p:transition` matches both `mc:Choice` and `mc:Fallback` on a morph slide, yielding `2 element(s) affected`): - # Fix (run in terminal): - # officecli set .pptx /slide[N]/shape[X] --prop x=36cm - ``` +```bash +# Per-slide: add spd="slow" to every transition element on slide N (2 XML hits per morph slide) +for N in 2 3 4; do + officecli raw-set "$FILE" "/slide[$N]" --xpath "//p:transition" --action setattr --xml 'spd=slow' +done +officecli validate "$FILE" +``` -3. **Visual issues**: - ```bash - # Open HTML preview to debug layout - officecli view .pptx html - ``` +Readback: `officecli query "$FILE" slide --json | jq '.data.results[].format | select(.transition=="morph") | .transitionSpeed'` prints `"slow"` for each affected slide. -**Note**: `!!scene-*` shapes (decoration/background actors) should appear on all slides — that's normal and expected. However, `!!actor-*` shapes (content actors) MUST be ghosted at section boundaries to prevent ghost accumulation. Only `#sN-` prefix shapes are checked by `morph_final_check`; `!!actor-*` shapes require screenshot verification to confirm they are off-screen after their section ends. +## Outputs & delivery ---- +Every morph deck ships with three artifacts, each as a standalone file: -### Phase 5: Iterate +1. `.pptx` — the deck, closed + `officecli validate` clean (Delivery Gate 1 OK). +2. `build.sh` or `build.py` — the re-runnable script (bash for shell-native builds; Python for multi-slide arcs using `morph-helpers.py`). Must recreate the deck from a fresh `officecli create` call. +3. `brief.md` — **standalone file, NOT embedded in anything else.** Contains: + - Section 1: topic / audience / purpose / narrative / style direction (1 named style from `reference/styles/INDEX.md`) + - Section 2: slide-by-slide outline (page type + one-sentence argument per slide) + - Section 3: §Morph Pair Planning table (Pair / Slide A / Slide B / Actors / Ghosts) — the design record the reviewer needs to audit choreography -Ask user for feedback, support quick adjustments. +**Pre-deliver reminder to the user (verbatim-safe wording):** ---- +- "The deck is ready with morph transitions. Open it in PowerPoint 365 / Keynote / WPS to see the motion — LibreOffice and web viewers render static." +- "While the build script is running, the `.pptx` may be rewritten several times. If you want to preview progress, use `officecli watch "$FILE"` and open the live preview in AionUi — do NOT click 'Open with system app' during the build, or you'll hit a file lock." -## References +## Adjustments after creation -- `reference/decision-rules.md` — Planning logic, Pyramid Principle -- `reference/pptx-design.md` — Design principles (Canvas, Fonts, Colors, Scene Actors, Page Types) -- `reference/officecli-pptx-min.md` — Tool syntax -- `reference/styles/INDEX.md` — Visual style examples organized by use case +Standard adjustments table → see pptx v2 §Common Pitfalls / `swap` / `move` / `remove` / `set`. Morph caveat: **after any `swap` or `move` that reorders morph-paired slides, re-verify the adjacency of shared `!!` names.** Run Gate 5b-morph-3 query above on the affected pairs — if the swap broke a pair, either rename shapes or re-choreograph the transition. ---- +**Final sanity check before delivery.** Run the full Delivery Gate (1 through 5b-morph-1..4), open the `.pptx` in PowerPoint 365 / Keynote / WPS, watch one full slide-to-slide morph to confirm motion is visible. If any Gate prints REJECT, fix and re-run — never deliver with a known-open gate. -**First time?** Read "Understanding Morph" above, skim one style reference for inspiration, then generate. Always use `morph-helpers.py` workflow. You'll learn by doing. +## References -**Trust yourself.** You have vision, design sense, and the ability to iterate. These tools enable you — your creativity makes it excellent. +- `reference/decision-rules.md` — Pyramid Principle, SCQA, page-type menu, `brief.md` schema. Read during §Morph Pair Planning to decide narrative arc before writing commands. +- `reference/pptx-design.md` — residual design notes (Scene Actors mechanics, page-type table, choreography patterns). Canvas / fonts / colors live in pptx v2 — this file covers only the morph-unique material. +- `reference/morph-helpers.py` — Cross-platform (Mac / Windows / Linux) Python helpers for clone + ghost + verify + final-check. Import as a library or call via CLI args. Preferred for 5+ slide arcs. +- `reference/morph-helpers.sh` — Bash equivalent. Pick one per project; do not mix. +- `reference/styles/INDEX.md` — 52-style visual library, grouped by palette (dark / light / warm / vivid / bw / mixed) and mood. Lookup workflow in §Style library lookup workflow above. +- `skills/officecli-pptx/SKILL.md` — base pptx v2 rules (visual floor, grid, canonical palettes, chart-choice, connector canon, Delivery Gate 1–5a, Known Issues C-P-1..7, Shell escape 3-layer). diff --git a/skills/morph-ppt/reference/decision-rules.md b/skills/morph-ppt/reference/decision-rules.md index 7b84b0657..e7230bcba 100644 --- a/skills/morph-ppt/reference/decision-rules.md +++ b/skills/morph-ppt/reference/decision-rules.md @@ -1,10 +1,12 @@ --- name: decision-rules -description: PPT Planner — Infer Audience/Purpose/Narrative, Build brief.md (outline + page briefs) +description: "Planning prompt for PPT — infer audience, purpose, narrative, then emit brief.md. Run before the main recipes when the deck's audience or purpose is underspecified." --- # PPT Planner +**How to use.** Read this file during `SKILL.md` §Morph Pair Planning, **before** writing any `officecli add / set` command. Infer audience, purpose, and narrative from the user's topic; emit a single `brief.md` that the main recipes will consume. A morph arc without a narrative spine collapses into "slide with motion" instead of "story with motion" — the planning below prevents that. + Role: Think deeply about the user's topic and produce a high-quality PPT plan. Output: A single `brief.md` containing extraction summary, outline, and detailed page briefs. diff --git a/skills/morph-ppt/reference/morph-helpers.py b/skills/morph-ppt/reference/morph-helpers.py index 3a224ab9d..84d88f981 100644 --- a/skills/morph-ppt/reference/morph-helpers.py +++ b/skills/morph-ppt/reference/morph-helpers.py @@ -69,8 +69,8 @@ def _collect_shapes(children, callback): """Walk a shape tree depth-first, calling callback(child) for each node.""" for child in children: callback(child) - if "Children" in child: - _collect_shapes(child["Children"], callback) + if "children" in child: + _collect_shapes(child["children"], callback) # --------------------------------------------------------------------------- @@ -149,14 +149,14 @@ def _check_unghosted(data, prev_slide): unghosted = [] def visit(child): - name = child.get("Format", {}).get("name", "") - x = child.get("Format", {}).get("x", "") - path = child.get("Path", "") + name = child.get("format", {}).get("name", "") + x = child.get("format", {}).get("x", "") + path = child.get("path", "") if f"#s{prev_slide}-" in name and x != "36cm": unghosted.append(f"{path}: name={name}, x={x}") - if "Children" in data: - _collect_shapes(data["Children"], visit) + if "children" in data: + _collect_shapes(data["children"], visit) return unghosted @@ -169,13 +169,13 @@ def extract(data): boxes = [] def visit(child): - if child.get("Type") != "textbox": + if child.get("type") != "textbox": return - name = child.get("Format", {}).get("name", "") - text = child.get("Text", "").strip() - x = child.get("Format", {}).get("x", "") - y = child.get("Format", {}).get("y", "") - path = child.get("Path", "") + name = child.get("format", {}).get("name", "") + text = child.get("text", "").strip() + x = child.get("format", {}).get("x", "") + y = child.get("format", {}).get("y", "") + path = child.get("path", "") if not text or len(text) < 6: return @@ -187,8 +187,8 @@ def visit(child): if has_slide_pattern or not is_scene: boxes.append({"path": path, "text": text[:50], "x": x, "y": y}) - if "Children" in data: - _collect_shapes(data["Children"], visit) + if "children" in data: + _collect_shapes(data["children"], visit) return boxes prev_boxes = extract(prev_data) @@ -253,8 +253,9 @@ def morph_verify_slide(deck, slide): has_error = True else: print(f"{GREEN} No unghosted content detected{NC}") - except Exception: - print(f"{GREEN} No unghosted content detected{NC}") + except Exception as e: + print(f"{RED} [helper] unghosted-check parse error: {e}{NC}", file=sys.stderr) + has_error = True # Method 2: duplicate text/position detection (backup for missing # prefix) try: @@ -272,8 +273,9 @@ def morph_verify_slide(deck, slide): print(f"{YELLOW} 2. Forgot to ghost previous slide's content{NC}") print(f"{YELLOW} 3. Forgot to add new content for this slide{NC}") has_error = True - except Exception: - pass + except Exception as e: + print(f"{RED} [helper] duplicate-check parse error: {e}{NC}", file=sys.stderr) + has_error = True if not has_error: print(f"{GREEN}Slide {slide} verification passed{NC}") @@ -291,6 +293,8 @@ def morph_verify_slide(deck, slide): def morph_final_check(deck): """Verify the entire deck: all slides (2+) must pass morph_verify_slide. + Also checks for M-2 ghost accumulation (shapes piled up at x≥34cm). + Args: deck: path to .pptx file @@ -314,6 +318,24 @@ def morph_final_check(deck): print(f"Total slides: {total_slides}") print() + # --- New: Check for M-2 ghost accumulation --- + print(f"{BLUE}Checking ghost accumulation (M-2)...{NC}") + rc, out, _ = _run("officecli", "query", deck, "shape[x>=34cm]", "--json") + try: + data = json.loads(out).get("data", {}) + ghost_count = len(data.get("results", [])) + expected_max = max(50, total_slides * 4) # ~4 actors × slides + + if ghost_count > expected_max: + print(f"{RED} REJECT: Found {ghost_count} accumulated ghost shapes (expected ≤ {expected_max}){NC}") + print(f"{RED} This is M-2 ghost accumulation — shapes moved to x≥34cm but not cleaned per-slide.{NC}") + print(f"{RED} See §Ghost Discipline & Actor Lifecycle in SKILL.md.{NC}") + return False + else: + print(f"{GREEN} Ghost count OK: {ghost_count} shapes (≤ {expected_max}){NC}") + except Exception as e: + print(f"{YELLOW} Warning: could not parse ghost count: {e}{NC}") + error_count = 0 for i in range(2, total_slides + 1): if not morph_verify_slide(deck, i): @@ -334,6 +356,48 @@ def morph_final_check(deck): # CLI entry point # --------------------------------------------------------------------------- +def clean_ghost_accumulation(deck, threshold=50): + """Remove ghost shapes exceeding threshold (M-2 fix). + + Deletes shapes at x≥34cm, keeping only the first N (buffer for morph exit). + + Args: + deck: path to .pptx + threshold: max ghosts to keep (default 50) + + Returns: + Number of shapes deleted + """ + print(f"{BLUE}Cleaning ghost accumulation...{NC}") + + rc, out, _ = _run("officecli", "query", deck, "shape[x>=34cm]", "--json") + try: + data = json.loads(out).get("data", {}) + results = data.get("results", []) + ghost_count = len(results) + + if ghost_count <= threshold: + print(f"{GREEN} Ghost count already OK: {ghost_count} ≤ {threshold}{NC}") + return 0 + + # Sort by slide (ascending) so we delete oldest/leftmost first + to_delete = results[threshold:] + print(f"{YELLOW} Deleting {len(to_delete)} shapes (keeping {threshold})...{NC}") + + for shape in to_delete: + shape_id = shape.get("format", {}).get("id") + shape_name = shape.get("format", {}).get("name", "?") + if shape_id: + _run("officecli", "remove", deck, f"/shape[@id={shape_id}]") + print(f" Removed: {shape_name} ({shape_id})") + + print(f"{GREEN} Cleaned {len(to_delete)} shapes. Verify with: final-check{NC}") + return len(to_delete) + except Exception as e: + print(f"{RED} Error: {e}{NC}", file=sys.stderr) + return 0 + + def main(): parser = argparse.ArgumentParser( prog="morph-helpers.py", @@ -344,13 +408,15 @@ def main(): clone Clone slide and set morph transition ghost [idx ...] Ghost multiple shapes off-screen (x=36cm) verify Verify slide setup (transition + ghosting) - final-check Verify entire deck + final-check Verify entire deck (+ M-2 ghost accumulation check) + clean-accumulation Remove excess ghost shapes (M-2 fix) example: python morph-helpers.py clone deck.pptx 1 2 python morph-helpers.py ghost deck.pptx 2 7 8 9 python morph-helpers.py verify deck.pptx 2 python morph-helpers.py final-check deck.pptx + python morph-helpers.py clean-accumulation deck.pptx """, ) sub = parser.add_subparsers(dest="command") @@ -372,6 +438,9 @@ def main(): p = sub.add_parser("final-check") p.add_argument("deck") + p = sub.add_parser("clean-accumulation") + p.add_argument("deck") + args = parser.parse_args() if args.command == "clone": @@ -384,6 +453,8 @@ def main(): elif args.command == "final-check": if not morph_final_check(args.deck): sys.exit(1) + elif args.command == "clean-accumulation": + clean_ghost_accumulation(args.deck) else: parser.print_help() diff --git a/skills/morph-ppt/reference/morph-helpers.sh b/skills/morph-ppt/reference/morph-helpers.sh index 0d0281f8b..dbe9cfab6 100755 --- a/skills/morph-ppt/reference/morph-helpers.sh +++ b/skills/morph-ppt/reference/morph-helpers.sh @@ -146,9 +146,9 @@ try: def check_children(children, prev_slide): unghosted = [] for child in children: - name = child.get('Format', {}).get('name', '') - x = child.get('Format', {}).get('x', '') - path = child.get('Path', '') + name = child.get('format', {}).get('name', '') + x = child.get('format', {}).get('x', '') + path = child.get('path', '') # Check if this shape has previous slide's content prefix if f'#s{prev_slide}-' in name: @@ -157,13 +157,13 @@ try: unghosted.append(f\"{path}: name={name}, x={x}\") # Recursively check children - if 'Children' in child: - unghosted.extend(check_children(child['Children'], prev_slide)) + if 'children' in child: + unghosted.extend(check_children(child['children'], prev_slide)) return unghosted - if 'Children' in data.get('data', {}): - unghosted = check_children(data['data']['Children'], $prev_slide) + if 'children' in data.get('data', {}): + unghosted = check_children(data['data']['children'], $prev_slide) if unghosted: for item in unghosted: @@ -171,9 +171,10 @@ try: sys.exit(1) sys.exit(0) -except Exception: - sys.exit(0) -" 2>/dev/null) +except Exception as e: + print(f'[helper] parse error: {e}', file=sys.stderr) + sys.exit(2) +") local python_exit=$? if [ $python_exit -eq 1 ] && [ -n "$unghosted_check" ]; then @@ -204,12 +205,12 @@ try: boxes = [] def walk(children): for child in children: - if child.get('Type') == 'textbox': - name = child.get('Format', {}).get('name', '') - text = child.get('Text', '').strip() - x = child.get('Format', {}).get('x', '') - y = child.get('Format', {}).get('y', '') - path = child.get('Path', '') + if child.get('type') == 'textbox': + name = child.get('format', {}).get('name', '') + text = child.get('text', '').strip() + x = child.get('format', {}).get('x', '') + y = child.get('format', {}).get('y', '') + path = child.get('path', '') # Skip empty text and very short text if not text or len(text) < 6: @@ -237,11 +238,11 @@ try: 'y': y }) - if 'Children' in child: - walk(child['Children']) + if 'children' in child: + walk(child['children']) - if 'Children' in data.get('data', {}): - walk(data['data']['Children']) + if 'children' in data.get('data', {}): + walk(data['data']['children']) return boxes prev_boxes = extract_textboxes(prev_data, $prev_slide) @@ -266,9 +267,10 @@ try: sys.exit(1) sys.exit(0) -except Exception: - sys.exit(0) -" 2>/dev/null) +except Exception as e: + print(f'[helper] parse error: {e}', file=sys.stderr) + sys.exit(2) +") local dup_exit=$? diff --git a/skills/morph-ppt/reference/officecli-pptx-min.md b/skills/morph-ppt/reference/officecli-pptx-min.md deleted file mode 100644 index c40bfdd10..000000000 --- a/skills/morph-ppt/reference/officecli-pptx-min.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -name: officecli-commands -description: OfficeCli Command Reference — PPT generation and validation commands ---- - -# OfficeCli PPT Command Reference - -## 0) BEFORE YOU START (CRITICAL) - -**Every time before using officecli, run this check:** - -```bash -# Check if installed -if ! command -v officecli &> /dev/null; then - echo "Installing officecli..." - # macOS/Linux - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - # Windows: irm https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.ps1 | iex -else - # Check if update needed - CURRENT=$(officecli --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - LATEST=$(curl -fsSL https://api.github.com/repos/iOfficeAI/OfficeCLI/releases/latest | grep '"tag_name"' | sed -E 's/.*"v?([0-9.]+)".*/\1/') - - if [ "$CURRENT" != "$LATEST" ]; then - echo "Upgrading officecli $CURRENT → $LATEST..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - else - echo "officecli $CURRENT is up to date" - fi -fi - -# Verify version -officecli --version -``` - -**Why:** This ensures you have the latest features and bug fixes before starting work. - ---- - -## 1) Learn Commands from CLI (NOT from this doc) - -**DO NOT memorize property lists from this document.** They will become outdated. Instead, query the CLI for real-time syntax: - -### Three-Layer Help System - -```bash -# Layer 1: See all settable elements -officecli pptx set - -# Layer 2: See all properties for ONE element -officecli pptx set shape # Shows: text, fill, x, y, width, height, font, size, bold... -officecli pptx set slide # Shows: background, transition, advanceTime... -officecli pptx set chart # Shows: chartType, categories, data, style... - -# Layer 3: See format details for ONE property -officecli pptx set shape.fill # Color format examples -officecli pptx set shape.animation # Animation syntax details -officecli pptx set shape.gradient # Gradient syntax -officecli pptx set shape.shadow # Shadow format -``` - -### Workflow Example: Discover → Apply → Validate - -```bash -# Step 1: What can I modify on shapes? -$ officecli pptx set shape -# Output: text, fill, x, y, width, height, font, size, bold, italic, gradient, shadow... - -# Step 2: How do I use gradient? -$ officecli pptx set shape.gradient -# Output: gradient="linear:90:FF0000,0000FF" or "radial:FF0000,0000FF" - -# Step 3: Apply it -officecli set demo.pptx '/slide[1]/shape[1]' --prop gradient="linear:90:FF0000,0000FF" - -# Step 4: Validate -officecli validate demo.pptx -``` - -**For comprehensive reference:** https://github.com/iOfficeAI/OfficeCli/wiki/agent-guide - ---- - -## 2) Quick Start (10 Most Common Commands) - -Only memorize these essentials. For anything else, use `officecli pptx set/add`: - -```bash -# 1. Create document -officecli create deck.pptx - -# 2. Add slide with background and transition -officecli add deck.pptx '/' --type slide --prop background=1A1A2E --prop transition=morph - -# 3. Add shape/textbox -officecli add deck.pptx '/slide[1]' --type shape --prop text="Hello World" \ - --prop x=5cm --prop y=8cm --prop width=10cm --prop height=3cm --prop fill=FF0000 - -# 4. Modify existing shape -officecli set deck.pptx '/slide[1]/shape[1]' --prop fill=0000FF --prop text="New text" - -# 5. Clone slide (with all shapes) -officecli add deck.pptx '/' --from '/slide[1]' - -# 6. View structure -officecli view deck.pptx outline - -# 7. HTML preview (NEW in 1.0.14) - auto-opens in browser -officecli view deck.pptx html - -# 8. Get element details (check indices before modifying) -officecli get deck.pptx '/slide[1]' --depth 1 - -# 9. Validate document -officecli validate deck.pptx - -# 10. Performance mode (for 3+ commands on same file) -officecli open deck.pptx -officecli set deck.pptx '/slide[1]/shape[1]' --prop fill=FF0000 -officecli set deck.pptx '/slide[1]/shape[2]' --prop text="Fast" -officecli close deck.pptx -``` - -### More Element Types (when needed, check syntax with `officecli pptx add`) - -```bash -officecli add deck.pptx '/slide[1]' --type picture --prop path=photo.jpg --prop width=12cm -officecli add deck.pptx '/slide[1]' --type chart --prop chartType=column --prop categories="Q1,Q2" --prop data="Sales:100,200" -officecli add deck.pptx '/slide[1]' --type table --prop rows=3 --prop cols=4 -officecli add deck.pptx '/slide[1]' --type connector --prop preset=straight --prop line=FF0000 -officecli add deck.pptx '/slide[1]' --type video --prop path=demo.mp4 --prop autoplay=true -``` - ---- - -## 3) Critical Rules (Read Before Any Command) - -### Syntax Pitfalls (Top 4 Mistakes) - -| ❌ Wrong | ✅ Correct | Why | -| ----------------------- | ------------------------------- | ------------------------------------------------ | -| `--name "foo"` | `--prop name="foo"` | All attributes go through `--prop` | -| `'/slide[@name="foo"]'` | `'/slide[1]/shape[3]'` | Numeric indexing only (1-based) | -| `x=-3cm` | `x=0cm` or `x=36cm` | No negative coordinates. Use 36cm for off-screen | -| Add shapes before slide | Create → add slide → add shapes | Must add slide first or "Slide not found" | - -### Shape Index Management - -**Indices change dynamically. Always verify before modifying:** - -```bash -# Check current indices -officecli get deck.pptx '/slide[1]' --depth 1 -# Output: shape[1] name="title", shape[2] name="subtitle", shape[3] name="logo"... -``` - -**Index behavior:** - -- **After clone:** New slide inherits all shapes with same indices as source -- **After add:** New shape gets next index (if slide has 8 shapes, new = `shape[9]`) -- **After remove:** Indices shift down (remove `shape[3]` → old `shape[4]` becomes `shape[3]`) - -### Morph Animation - -`transition=morph` creates smooth animations by **matching shapes by name** across adjacent slides. - -**How it works:** - -1. Adjacent slides must have shapes with **identical names** for morphing -2. PowerPoint matches by name and animates position/size/color changes -3. If names don't match → only fade in/out (no morph effect) - -**Morph variants:** - -```bash ---prop transition=morph # Match by object (default) ---prop transition=morph-byWord # Word-by-word text animation ---prop transition=morph-byChar # Character-by-character text animation -``` - -**For complete Morph workflow** (naming conventions, ghosting, helpers), see `SKILL.md` Phase 3. - -### Script Best Practices - -- **Multi-line text:** Use `\\n` (double-escaped in bash) or split into multiple textboxes -- **Line continuation (bash):** No trailing spaces after backslash `\` -- **Filenames:** Use English names to avoid encoding issues -- **Language choice:** Use bash/python/powershell — whatever executes `officecli` commands clearly - -### Batch JSON Mode (NOT RECOMMENDED for Morph) - -**Do NOT use** `officecli batch --input commands.json` for Morph presentations. -Reason: Morph requires careful step-by-step control that's hard to debug in JSON. - -If you must use batch mode for non-Morph tasks: - -- **Booleans as strings:** `{"props":{"bold":"true"}}` not `{"bold":true}` -- **Escape quotes:** `{"props":{"text":"It\\'s working"}}` - ---- - -## 4) Troubleshooting - -When a command fails: - -1. **Read the error message** — officecli provides descriptive errors - - "Unrecognized argument" → check `--prop` format - - "Slide not found" → add slide first - - "Could not find path" → verify file exists - -2. **Inspect current state:** - - ```bash - officecli get '/slide[N]' --depth 1 # List all shapes + indices - officecli view outline # Document structure - officecli view issues # Validation issues - ``` - -3. **Check command syntax:** - - ```bash - officecli pptx set # See available properties - officecli pptx add # See available element types - ``` - -4. **Common fixes:** - | Error | Solution | - |-------|----------| - | "Slide not found" | Run `officecli add '/' --type slide` first | - | "Unrecognized argument" | Use `--prop key=value` format, not `--key value` | - | Shape not where expected | Run `get` to verify current indices after add/remove | - | File locked | Close PPT in PowerPoint/WPS before running commands | - -5. **Still stuck?** Generate HTML preview to debug visually: - ```bash - officecli view deck.pptx html # Opens in browser with live preview - ``` - ---- - -**Remember:** This doc is a quick reference. For the latest syntax, **always query the CLI first** with `officecli pptx set/add`. diff --git a/skills/morph-ppt/reference/pptx-design.md b/skills/morph-ppt/reference/pptx-design.md index 079dc2c24..31ece11d6 100644 --- a/skills/morph-ppt/reference/pptx-design.md +++ b/skills/morph-ppt/reference/pptx-design.md @@ -1,332 +1,254 @@ --- name: pptx-design -description: Design principles and aesthetics for Morph PPTs +description: Morph-specific design notes — color + typography floor for deep-stage decks, plus Scene Actors / Page Types / Shape Index / Morph Animation Essentials --- -# Design Essentials +# Morph Design Essentials -**Philosophy**: Create dynamic, beautiful presentations by designing layout and motion together from the start. +`skills/officecli-pptx/SKILL.md` §Requirements / §Design Principles / §Visual delivery floor is the **source of truth for type hierarchy, contrast, and palette picking** in every pptx, morph or not. This file narrows that floor to the **stage-feel register** a morph deck typically shoots for: darker backgrounds, larger hero type, deeper opacity range for scene actors, and per-slide text-width generosity that survives `#sN-*` ghost churn. Where pptx SKILL.md already states a rule, the guidance here is an additive override **only if the slide is actively in a morph pair** — otherwise defer upward. --- -## 1) Canvas & Coordinates +## 1) Color Principles (morph-stage register) -- **Canvas**: 16:9 (33.87cm × 19.05cm) -- **Safe Margins**: left/right 1.2cm, top/bottom 0.8cm -- **Spacing Tokens**: 0.2, 0.4, 0.6, 0.8, 1.2, 1.6 cm (use these for consistency) -- **Ghost Position**: `x=36cm` (off the right edge) +### Contrast is King — always compute, never eyeball ---- - -## 2) Fonts & Typography - -### Recommended Combinations - -| Content Type | Primary Font | Fallback | -| ------------ | --------------------------------- | ----------------------------- | -| English | Montserrat (title) + Inter (body) | Segoe UI / Helvetica Neue | -| Chinese | Source Han Sans (思源黑体) | PingFang SC / Microsoft YaHei | -| Mixed | Montserrat + Source Han Sans | Segoe UI + System Font | - -### Size Scale - -- Title: 54-72pt, bold/black -- Heading: 28-40pt -- Body: 18-24pt -- Caption: 13-16pt (minimum 13pt) - -### Text Width Guidelines - -**CRITICAL: Always make text boxes wider than you think necessary. Wrapping breaks visual hierarchy.** - -| Content Type | Minimum Width | Best Practice | -| -------------------------------- | ------------- | ----------------------------------------------------------- | -| **Centered titles (64-72pt)** | 28cm | Use 28-30cm for 10-15 char titles, 25cm for hero statements | -| **Centered subtitles (28-40pt)** | 25cm | Always use 25-28cm to avoid mid-word breaks | -| **Left-aligned titles** | 20cm | Use 20-25cm depending on content length | -| **Body text / cards** | 8cm (single) | Single-column 8-12cm, double-column 16-18cm | - -**Common mistakes to avoid**: - -- ❌ Using 10-15cm for long centered subtitles → causes awkward line breaks -- ❌ Tight text boxes that "just fit" the text → one extra character breaks layout -- ✅ Always add 3-5cm extra width for centered text -- ✅ Test with slightly longer text in your mind - -**Rule of thumb**: When in doubt, make text boxes wider. Extra whitespace is better than wrapped text overlapping other elements. - ---- - -## 3) Color Principles - -### Contrast is King - -Text must be readable: - -- **Dark background** (brightness < 128) → white or light text (#FFFFFF, #EEEEEE) -- **Light background** (brightness ≥ 128) → dark text (#000000, #333333) - -**Brightness formula**: +Morph decks lean dark; mid-gray body text (`#666666`) that reads fine in a pptx base render **disappears under projector glare** the moment the backdrop goes below brightness 30. Compute before you pick: ``` Brightness = (R × 299 + G × 587 + B × 114) / 1000 ``` -**Examples**: +Deployment rule (morph-specific — stricter than pptx base): -- `#000000` (black) = 0 → dark → use white text -- `#2C3E50` (dark blue) = 62 → dark → use white text -- `#E74C3C` (red) = 115 → dark → use white text -- `#F39C12` (orange) = 160 → light → use dark text -- `#FFFFFF` (white) = 255 → light → use dark text +- **Dark background** (brightness < 128) → body text brightness ≥ 80% (`#FFFFFF`, `#EEEEEE`, `#CADCFC`). Chart series fills + icon strokes must clear the same floor. +- **Light background** (brightness ≥ 128) → body text brightness ≤ 20% (`#000000`, `#333333`). +- **Mixed / gradient background** — add a semi-transparent backing block (`opacity=0.3-0.6`) behind the run of text; do not rely on the gradient to "average out". -**Safe combinations**: +Worked samples: -- White text (#FFFFFF) on dark backgrounds (#000000–#555555) -- Black/dark gray text (#000000–#333333) on light backgrounds (#EEEEEE–#FFFFFF) -- For mixed backgrounds: add a semi-transparent backing block behind the text +- `#000000` brightness 0 → dark → white text +- `#1E2761` brightness 35 → dark → white text +- `#2C3E50` brightness 62 → dark → white text +- `#E94560` brightness 88 → still dark → white text (common mistake: treating bright red as "mid") +- `#F39C12` brightness 160 → light → dark text +- `#FFFFFF` brightness 255 → light → dark text -**Tip**: When in doubt, choose high contrast — it's always more readable. +**When in doubt, push contrast.** Stage-style decks are read under projector + mixed ambient light — reviewer's monitor comfort is not the right benchmark. -### Color Hierarchy +### Color Hierarchy — three depth layers -Maintain three-layer visual hierarchy: +A morph deck has more visible elements per frame than a pptx base slide (scene actors + content + chart series + annotations). Hold the stack: ``` -Background → Decorative Shapes → Content (text/data) -(weakest) (medium) (strongest) +Background fill → Scene actors → Content (text / data / KPI) +(weakest) (medium) (strongest) ``` -**Decorative shape opacity**: +Opacity ranges for `!!scene-*` and `!!actor-*` shapes (morph-specific — tighter than pptx base): -- ≤ 0.12 for background decoration (let content shine) -- 0.3-0.6 for content backgrounds (evidence slides, data cards) +- **≤ 0.12** — whole-deck decoration (`!!scene-grid`, `!!scene-band`, corner accents). Must not compete with content at the back of the room. +- **0.3 – 0.6** — evidence / data backing blocks (`!!actor-evidence-bg`, KPI card fills). Strong enough to frame, soft enough to let numbers shine. +- **0.8 – 1.0** — reserved for `!!actor-*` shapes that ARE the content (a hero ring behind a single stat, a brand color strip as the message). Use sparingly — more than 2 per slide reads as clutter. -### Palette Selection +A scene actor that lands on `opacity=0.7` in the content core is usually a mis-classified actor; either lower it (it's decoration) or rename it `!!actor-*` (it's content) and plan an exit slide. -**Create unique palettes based on topic mood** — there are no universal formulas. +### Palette Selection — pick for mood, not for habit -**Need inspiration?** Browse `reference/styles/` for color combinations across different moods (dark, light, warm, vivid, bw, mixed). +There are no universal palette formulas for morph decks. The four pptx canonical palettes (Executive navy / Forest & moss / Warm terracotta / Charcoal minimal) still apply, but morph decks pick more freely from the 52-style library because cross-slide motion amplifies color mood. ---- +Decision path: -## 4) Scene Actors (Animation Engine) +1. **Match topic mood** → tech / fintech lean `dark--*`; healthcare / education lean `light--*` or `warm--*`; design / brand lean `bw--*` or `mixed--*`. +2. **Respect user-specified hex** → if the brief names a brand color, scan `reference/styles/INDEX.md` Quick Lookup for the nearest hex trio; do not force-fit the mood label. +3. **Vary by project** — avoid repeating the last three decks' palette family. `dark--premium-navy` on every pitch deck reads as a template, not a design choice. +4. **Name the palette in `brief.md`** → "warm--earth-organic palette" is a commitment; "warm tones" is not. -**Purpose**: Create smooth Morph animations through persistent shapes that change properties. +Use `reference/styles/` for inspiration (palette + signature gesture), **not** for coordinates — per `reference/styles/INDEX.md` L5-11, the build.sh coordinates are hand-tuned for demo content. -### Setup - -Define 6-8 actors on Slide 1: - -- **Large** (5-8cm): Main visual anchors -- **Medium** (2-4cm): Supporting elements -- **Small** (1-2cm): Accents and details - -**Shape types**: ellipse, rect, roundRect, triangle, diamond, star5, hexagon - -**Naming conventions (recommended for best results)**: +--- -1. **Scene actors** (persistent shapes): +## 2) Typography (morph-stage register) - ```bash - --prop 'name=!!dot-main' # Single quotes prevent shell ! escaping - --prop 'name=!!line-top' - --prop 'name=!!slash-accent' - ``` +### Recommended Combinations - - Pattern: `!!` prefix (double exclamation) - - These shapes persist and morph across all slides +Morph decks are often viewed on stage or in projector-heavy settings where font weight carries farther than font choice. Two fonts max — one for headings, one for body. -2. **Content shapes** (unique per slide): +| Content Type | Primary Pair | Fallback | +| ------------ | ----------------------------------------- | --------------------------------- | +| English | Montserrat (title) + Inter (body) | Segoe UI / Helvetica Neue | +| Chinese | Source Han Sans 思源黑体 (title + body) | PingFang SC / Microsoft YaHei | +| Mixed CN/EN | Montserrat + Source Han Sans | Segoe UI + System Font | - ```bash - --prop 'name=#s1-title' # Format: # + s + slide_number + - + description - --prop 'name=#s2-card1' - --prop 'name=#s3-stats' - ``` +Avoid Georgia / Times for body on morph slides — serif terminals disappear when the shape interpolates mid-motion. Reserve serif for pptx base decks with no transition movement. - - Pattern: `#sN-` prefix (where N = slide number) - - These shapes are ghosted when moving to next slide +### Size Scale — one notch larger than pptx base -**Benefits of following these patterns:** +A morph deck is read from farther back (stage setups, large screens) and each frame holds motion in addition to text. Size up: -- Primary verification (pattern matching) is fastest and catches all cases -- Code is self-documenting: `#s2-card1` clearly means slide 2's first card -- Easy to debug: search for `#s1-` to find all slide 1 content -- Backup verification (duplicate detection) exists but has edge cases +| Role | pptx base | morph-stage (use this) | +| ------------------- | ---------- | ----------------------- | +| Hero / cover title | 44-60pt | **54-72pt**, bold/black | +| Section heading | 24-32pt | **28-40pt**, bold | +| Body / supporting | 16-22pt | **18-24pt** | +| Caption / footnote | 12-14pt | **13-16pt** (floor 13) | -**Note**: In Python build scripts, `#` and `!!` require no special quoting — pass them as plain strings (e.g. `"--prop", "name=#s1-title"`). +Do not drop below 13pt on any slide — projector glare erodes the lowest two point sizes first. -### How Morph Pairing Works +### Text Width Guidelines — widen for centered, widen for ghost churn -- PowerPoint matches shapes by **name** across adjacent slides -- Same name + different properties = smooth animated transition -- To hide a scene actor on a slide, move it off-screen: `x=36cm` (ghost position, right of canvas) -- To bring it back, move it to a visible position on the next slide +Wrapping breaks visual hierarchy in a static deck; in a morph deck it **also breaks the motion** (the interpolation picks up the wrapped baseline and the text appears to tilt mid-transition). Make text boxes wider than you think. -**Example** — 3 scene actors across 3 slides: +| Content Type | Minimum Width | Best Practice | +| -------------------------------- | ---------------- | ----------------------------------------------------------- | +| Centered titles (64-72pt) | 28cm | 28-30cm for 10-15 char titles, 25cm for hero statements | +| Centered subtitles (28-40pt) | 25cm | Always 25-28cm to avoid mid-word breaks | +| Left-aligned titles | 20cm | 20-25cm depending on content length | +| Body text / cards | 8cm (single) | Single-column 8-12cm, double-column 16-18cm | +| Ghost-target content (`#sN-*`) | same as source | Width must match the on-slide version — a narrower ghost pulls the morph into a resize-plus-move tilt | -``` -Slide 1: dot-main (x=2cm, y=3cm), line-top (x=5cm, y=1cm), slash-accent (x=10cm, rotation=30) -Slide 2: dot-main (x=8cm, y=10cm), line-top (x=15cm, y=5cm), slash-accent (x=20cm, rotation=60) -Slide 3: dot-main (x=36cm) [hidden], line-top (x=10cm, y=2cm), slash-accent (x=25cm, rotation=0) -``` +Common mistakes in morph decks: -### Evolution +- Using 10-15cm for long centered subtitles → awkward wrap + visible tilt during transition. +- Tight text boxes that "just fit" the text → one extra character on a cloned slide breaks layout. +- Ghost target (x=36cm) sized smaller than source → morph reads as a shrink-and-move instead of a slide-off. -On subsequent slides: +**Rule of thumb:** when in doubt, widen. Extra whitespace is better than wrapped text during a morph interpolation. -- **Position**: Move actors to different locations (create motion) -- **Size**: Grow or shrink (create emphasis) -- **Rotation**: Rotate for dynamic feel -- **Color/Opacity**: Subtle shifts (mood changes) -- **Hide when not needed**: Move to `x=36cm` (ghost position) +--- -**Key**: Adjacent slides should have **noticeably different** spatial compositions. +## 3) Scene Actors (Animation Engine) — expanded -### Content (added fresh per slide) +**Purpose.** Create smooth Morph animations through persistent shapes that change properties across adjacent slides. -Content (titles, body text, numbers, cards) is added fresh on each slide with `officecli add`. Since text changes every slide, Morph just cross-fades it — no benefit from same-name pairing. +### Setup -**Critical workflow**: +Define 6-8 actors on Slide 1 if the deck tells a continuous-visual story: -1. Clone previous slide → inherited content has old slide's prefix (e.g., `#s1-title`) -2. Ghost inherited content → move all `#s(N-1)-*` shapes to `x=36cm` -3. Add new content → with current slide's prefix (e.g., `#s2-title`) +- **Large** (5-8cm): Main visual anchors (hero circle, band, hero card) +- **Medium** (2-4cm): Supporting elements (metric cards, accent rings) +- **Small** (1-2cm): Accents and details (dots, dashes, icons) -**Why ghosting matters**: Without ghosting old content, slides accumulate shapes, causing visual overlap and confusion. +**Shape types** available via `--prop preset=`: `ellipse | rect | roundRect | triangle | diamond | star5 | hexagon`. Full list: `officecli help pptx shape`. -### Coordinate Notes +### Naming (SKILL.md is authoritative) -- Ghost position: `x=36cm` (off the right edge of the 33.87cm canvas) -- Spread y-coordinates for ghosted shapes: `y=0cm`, `y=5cm`, `y=10cm`, `y=15cm` -- Coordinates start at `x=0cm` — negative values are not supported +Three-prefix system — `!!scene-*` / `!!actor-*` / `#sN-*`. Source of truth: `SKILL.md` §What is Morph? — core mechanics. This file adds only the Python-vs-shell quoting note below. ---- +**Python:** `#` and `!!` require no special quoting — pass as plain strings in `subprocess.run([..., "--prop", "name=#s1-title", ...])`. -## 5) Page Types +**Shell (bash/zsh):** ALWAYS single-quote to avoid history expansion on `!!` and comment-leading on `#`: `--prop 'name=!!scene-ring'` / `--prop 'name=#s1-title'`. -Mix these to create rhythm. Each serves a different narrative purpose: +### Pairing example — 3 actors × 3 slides -| Type | When to Use | Visual Structure | -| -------------- | ----------------------- | ------------------------------------------------------------------ | -| **hero** | Opening, closing | Large centered title + scattered scene actors | -| **statement** | Key message, transition | One impactful sentence + dramatic actor shifts (8cm+ moves) | -| **pillars** | Multi-point structure | 2-4 equal columns, actors become card backgrounds (opacity 0.12) | -| **evidence** | Data, statistics | 1-2 large asymmetric blocks + supporting details (opacity 0.3-0.6) | -| **timeline** | Process, sequence | Horizontal or vertical flow with step backgrounds | -| **comparison** | A vs B | Left-right split (50/50 or 60/40) with contrasting colors | -| **grid** | Multiple items | Scattered or grid layout, lighter feel | -| **quote** | Breathing moment | Centered text, minimal decoration | -| **cta** | Call to action | Return to bold, centered design | -| **showcase** | Featured display | Large central area for product/screenshot | +``` +Slide 1: !!scene-ring (x=5cm, y=3cm, w=8cm, fill=E94560, opacity=0.3) + !!scene-dot (x=28cm, y=15cm, w=1cm) + !!actor-headline (x=4cm, y=8cm, w=26cm, size=48) + +Slide 2: !!scene-ring (x=20cm, y=2cm, w=12cm, opacity=0.6) ← same name, new position+size + !!scene-dot (x=3cm, y=16cm, w=1.5cm) ← moved to opposite corner + !!actor-headline (x=1.5cm, y=1cm, w=12cm, size=24) ← shrunk + moved to top-left + +Slide 3: !!scene-ring (x=36cm) ← ghosted off-canvas + !!scene-dot (x=10cm, y=2cm, w=1cm) + !!actor-headline (x=36cm) ← ghost: new headline takes over + !!actor-subpoint (x=4cm, y=8cm, w=26cm, size=36) ← new actor enters (no pair on S2 = fade in) +``` -**Variety matters**: Avoid repeating the same type consecutively. +### Per-slide content (`#sN-*`) workflow -**Design notes**: +1. **Clone previous slide** → inherited `#s(N-1)-*` content carries the old slide's prefix. +2. **Ghost inherited content** → move all `#s(N-1)-*` shapes to `x=36cm`. +3. **Add new content** → with current slide's prefix `#sN-*`. -- **pillars**: Multi-column layout with even distribution, scene actors morph into card backgrounds (roundRect, opacity=0.12) -- **evidence**: Asymmetric data layout, 1 large actor (30-40% canvas) + 1 medium (20-30%), opacity 0.3-0.6 allowed for data backgrounds -- **grid**: Must differ from pillars and evidence — light, scattered vs. structured +Without step 2, slides accumulate shapes → visual overlap compounds silently across the deck. --- -## 6) Style References - -Browse `reference/styles/` for design inspiration. See `reference/styles/INDEX.md` for a complete catalog organized by use case. - ---- +## 4) Page Types (mix for rhythm) -## 7) Shape Index Mechanics +Vary page types to avoid monotony. Each serves a different narrative purpose: -Shapes are numbered sequentially on each slide: `shape[1]`, `shape[2]`, `shape[3]`... +| Type | When to use | Visual structure | +|---|---|---| +| **hero** | Opening, closing | Large centered title + scattered scene actors | +| **statement** | Key message, transition | One impactful sentence + dramatic actor shifts (8cm+ moves) | +| **pillars** | Multi-point structure | 2-4 equal columns, actors become card backgrounds (opacity 0.12) | +| **evidence** | Data, statistics | 1-2 large asymmetric blocks + supporting details (opacity 0.3-0.6) | +| **timeline** | Process, sequence | Horizontal or vertical flow with step backgrounds | +| **comparison** | A vs B | Left-right split (50/50 or 60/40) with contrasting colors | +| **grid** | Multiple items | Scattered or grid layout, lighter feel | +| **quote** | Breathing moment | Centered text, minimal decoration | +| **cta** | Call to action | Return to bold, centered design | +| **showcase** | Featured display | Large central area for product/screenshot | -### Index Behavior +**Design notes:** -**On Slide 1**: Shapes added in order +- **pillars**: Multi-column even distribution; scene actors morph into card backgrounds (roundRect, opacity=0.12). +- **evidence**: Asymmetric — 1 large actor (30-40% canvas) + 1 medium (20-30%), opacity 0.3-0.6 allowed for data backgrounds. +- **grid**: Must differ from pillars and evidence — light, scattered vs. structured. +- **Variety matters**: Avoid repeating the same page type consecutively. -```bash -# Scene actors: shape[1-6] -# Content: shape[7+] -``` +--- -**After cloning**: New slide inherits all shapes with identical indices +## 5) Shape Index Mechanics -```bash -officecli add deck.pptx '/' --from '/slide[1]' # S2 now has shape[1-N] -``` +Shapes are numbered sequentially on each slide: `shape[1]`, `shape[2]`, `shape[3]`... When `transition=morph` is applied, CLI auto-prefixes `!!` to names — **use index paths after that** (see SKILL.md §Known Issues M-1). -**After adding**: New shapes get the next available index +### Index behavior -```bash -# If slide has 9 shapes, next add becomes shape[10] -``` +- **On creation:** Shapes added in order get increasing indices. +- **After cloning:** New slide inherits all shapes with identical indices. +- **After adding to a cloned slide:** New shapes get the next available index. +- **After modifying:** Index stays the same. -**After modifying**: Index stays the same +### Pattern for build scripts -```bash -officecli set deck.pptx '/slide[2]/shape[3]' --prop x=20cm # Still shape[3] ``` - -### Pattern for Build Scripts - -```bash -# Slide 1: 6 actors + 2 content = 8 shapes total -# Slide 2: Clone (8) → Ghost content (shape[7-8]) → Add new (shape[9+]) -# Slide 3: Clone (10 shapes) → Ghost content (shape[9-10]) → Add new (shape[11+]) +Slide 1: 6 actors + 2 content = 8 shapes total +Slide 2: Clone (8) → Ghost content (shape[7-8]) → Add new (shape[9+]) +Slide 3: Clone (10) → Ghost content (shape[9-10]) → Add new (shape[11+]) ``` -**Formula**: Next slide's first new shape index = Previous slide's total shape count + 1 +**Formula:** Next slide's first new shape index = Previous slide's total shape count + 1. -**Debugging**: Use `officecli get '/slide[N]' --depth 1` to inspect actual indices. +**Debugging:** `officecli get $FILE '/slide[N]' --depth 1` to inspect actual indices. --- -## 8) Morph Animation Essentials +## 6) Morph Animation Essentials -### Minimum Requirements +### Minimum requirements -1. **Slides 2+ must have `transition=morph`** -2. **Scene actors must have identical names across slides** (`!!` prefix) -3. **Previous content must be ghosted** (`x=36cm`) before adding new content -4. **Adjacent slides should have different spatial layouts** +1. Slides 2+ must have `transition=morph` (`officecli set /slide[N] --prop transition=morph`). +2. Scene actors must have identical `name=` across slides. +3. Previous per-slide content must be ghosted (`x=36cm`) before adding new content. +4. Adjacent slides should have different spatial layouts (displacement ≥ 5cm OR rotation ≥ 15° OR size delta ≥ 30% on ≥ 3 shapes). -### Creating Motion +### Creating motion -Change at least 3 scene actors between adjacent slides: +Change ≥ 3 scene-actor properties between adjacent slides: - Move positions (x, y) - Resize (width, height) -- Rotate (rotation) +- Rotate (rotation degrees) - Shift colors (fill, opacity) -**Goal**: Create a sense of movement and transformation, not just fade in/out. +**Goal:** Sense of movement + transformation, not just fade. -### Entrance Effects +### Entrance effects on morph slides -- Morph handles shape transitions automatically — entrance animations are usually unnecessary -- If an entrance is needed, use the `with` trigger so it plays simultaneously with morph: `animation=fade-entrance-300-with` - -### Animation Format (if needed) +Morph handles shape transitions automatically — entrance animations are usually unnecessary. If one is needed (e.g., fade a new `#sN-*` card in), use the `with` trigger so it plays simultaneously with morph: ``` -Format: EFFECT[-DIRECTION][-DURATION][-TRIGGER][-delay=N][-easein=N][-easeout=N] - ---- - -## Design Freedom - -**This document provides principles, not prescriptions.** - -- Trust your design judgment -- Learn from style references -- Experiment with color and layout -- Iterate based on visual results -- Let the content guide the design +animation=fade-entrance-300-with +``` -**The best presentations come from understanding principles, then applying them creatively to your specific topic.** +Format: `EFFECT[-DIRECTION][-DURATION][-TRIGGER]`. See `officecli help pptx animation` for preset list. --- -Good design! 🎨 -``` +## 7) Style References + +52 visual style directories in `reference/styles/` — see `reference/styles/INDEX.md` for the catalog. Lookup workflow is in SKILL.md §Style library lookup workflow. Key rule: **learn the approach, do not copy coordinates** (the style build.sh files have known typesetting bugs per `INDEX.md` L5-11). diff --git a/skills/morph-ppt/reference/quality-gates.md b/skills/morph-ppt/reference/quality-gates.md deleted file mode 100644 index 4dae385a2..000000000 --- a/skills/morph-ppt/reference/quality-gates.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -name: quality-gates -description: Quality Reviewer — Check Content/Layout/Morph and provide fix guidance ---- - -# Quality Reviewer - -Role: Evaluate the quality of the generated PPT, identify issues, and guide fixes. - -Goal: Ensure the delivered PPT has clear content, comfortable layout, and smooth animations. - ---- - -## Content Gate - -### Check Criteria - -- ✅ 1 headline per slide -- ✅ Title <= 2 lines -- ✅ 3–5 bullet points -- ✅ No long paragraphs -- ✅ Conclusion First (title is an argument, not a topic) - -### Common Issues & Fixes - -| Issue | Fix | -| --------------------------------------- | ------------------------------------------------------------------------- | -| Title exceeds 2 lines | Shorten the text or reduce font size (64pt → 56pt) | -| Too many bullet points (>5) | Merge similar points or split into two slides | -| Title is a topic instead of an argument | Rewrite as a conclusion: "Cost reduced by 40%" instead of "Cost Analysis" | -| Long paragraph present | Break into 3–5 bullet points, 1–2 lines each | - ---- - -## Layout Gate - -### Check Criteria - -- ✅ Text boxes <= 14 per slide -- ✅ No overlapping text boxes -- ✅ x-coordinates aligned to grid lines -- ✅ scene actors opacity <= 0.12 (background decoration transparency) -- ✅ **Text color has sufficient contrast with background (readability)** ← mandatory check - -### Text Readability Check (critical) - -**Check Flow**: - -1. Get the `color` attribute of each text box -2. Get the background color at the text box's position (slide background or scene actor fill) -3. Determine whether the text color and background color provide sufficient contrast - -**Criteria** (using brightness formula): - -``` -Brightness = (R × 299 + G × 587 + B × 114) / 1000 - -- Brightness < 128 → Dark background → Text must be light (#FFFFFF) -- Brightness >= 128 → Light background → Text must be dark (#000000 or #333333) -``` - -**Examples**: - -- `#2C3E50` (dark blue) = 62 → Dark → Use white text -- `#E74C3C` (red) = 115 → Dark → Use white text -- `#F39C12` (orange) = 160 → Light → Use black text -- `#FFFFFF` (white) = 255 → Light → Use black text - -**Prohibited Errors**: - -- ❌ Dark blue text on dark blue background (similar or identical color values) -- ❌ White text on light background (insufficient contrast) -- ❌ Any case where text color = background color - -### Common Issues & Fixes - -| Issue | How to Identify | Fix | -| --------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------- | -| **Text color = background color** | Text color and background fill are identical | Dark background → change text to FFFFFF; Light background → change text to 000000 | -| **Insufficient contrast** | Text color and background color are both dark or both light | Invert one of them: dark background → white text; light background → black text | -| **Text wrapping overflow** | Text box too narrow, text forced to wrap and overflows | Increase text box width, or reduce text content | -| **Previous slide text residue** | Previous slide's title has no ghost on the current slide | Move the unneeded headline/content actor to `x=36cm` | -| Text box overlap | Two text boxes with overlapping y-coordinates | Adjust with `officecli set '/shape[N]' --prop y=XXcm` | -| x-coordinate not aligned | x is not a grid multiple | Align to grid: 1.2cm, 2.4cm, 3.6cm... | -| scene actors obscuring text | Opacity too high (>0.12) | Lower transparency: `--prop opacity=0.08` | -| Too many text boxes (>14) | Count shapes with type=textbox | Merge similar content or simplify descriptions | - ---- - -## Morph Gate - -### Check Criteria - -- ✅ All slides 2+ have `transition=morph` set -- ✅ **All slides have identically named scene actors** (6–8 fixed actors present on every slide) -- ✅ Adjacent slides have noticeably different spatial layouts (shape position, size, and rotation vary) -- ✅ Each slide has enough scene shapes (6+) to create a sense of motion -- ✅ Actors that should not be visible are placed off-screen (ghost position: `x=36cm` or `x=36cm`) - -### Check Method - -Use `officecli get` to verify that scene actor names are consistent across adjacent slides: - -```bash -# Check shape names on slide 1 and slide 2 -officecli get .pptx '/slide[1]' --depth 1 | grep name -officecli get .pptx '/slide[2]' --depth 1 | grep name - -# You should see the same list of actor names (e.g., !!dot-main, !!line-top) -``` - -### Common Issues & Fixes - -| Issue | Fix | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **Morph has no transform effect, only fade** | **Critical issue**: Scene actor names differ between adjacent slides. Fix: Use identically named actors across all slides (e.g., `!!dot-main`); place unneeded ones at `x=36cm` off-screen | -| Adjacent slides look too similar | Adjust scene actor position/size/rotation to create visual difference (displacement >= 5cm or rotation >= 15°) | -| Not enough scene shapes | Add decorative geometric shapes (ellipses, rectangles, triangles) with consistent names | -| Transition not smooth | Verify `transition=morph` is set; verify actors use identical names | -| An actor disappears on a slide | Do not delete the actor — move it off-screen (`x=36cm` or `x=36cm`) instead | - ---- - -## Delivery Gate - -- ✅ Must pass `officecli validate .pptx` -- ✅ Must pass `officecli view outline .pptx` (structure is reasonable) -- ✅ Exactly 3 deliverables: `.pptx` + `build.sh` + `brief.md` (no other files) -- ✅ `build.sh` can be re-run to produce the same result - ---- - -## Check Flow - -### Per-Slide Check (during Phase 3 — mandatory) - -Self-check immediately after generating each slide. Fix issues before moving on. - -1. **Content**: Headline clear? Bullet points <= 5? -2. **Layout**: No overlaps? Text color contrasts with background? -3. **Morph**: Scene actors have same names as previous slide? Scene actors not needed are ghosted to `x=36cm`? -4. **⚠️ TEXT OVERLAP CHECK**: Previous slide's content actors are ALL ghosted (`x=36cm`)? This slide's new content is added fresh? - - Since content is added per slide (not pre-defined on slide 1), you only need to check the **previous slide's** content — not all slide types - - **This is the #1 most common defect** — failure causes visible text overlap - -**This is the primary quality gate.** If every slide passes, the PPT is already high quality. - -### Pre-Delivery Check (Phase 4 — two commands) - -After all slides are generated: - -```bash -officecli validate .pptx # must pass -officecli view outline .pptx # verify structure -``` - -If issues found → fix and re-validate (max 2 rounds). If still failing → report to user. - ---- diff --git a/skills/morph-ppt/reference/styles/INDEX.md b/skills/morph-ppt/reference/styles/INDEX.md index 945f91a38..e0b9fc986 100644 --- a/skills/morph-ppt/reference/styles/INDEX.md +++ b/skills/morph-ppt/reference/styles/INDEX.md @@ -11,89 +11,91 @@ The Agent uses this table to quickly select a reference style based on the topic --- +**Primary hex column**: bg / fg / accent — sampled from each style's `build.sh`. Use this to eyeball-match a user-specified brand color before opening any `style.md`. `-` = style has only `style.md` (no build script to extract from). + ## Dark Palette (dark) -| Directory | Style Name | Best For | Mood | -| ------------------------ | ------------------------ | --------------------------------------------------------------- | --------------------------------------- | -| dark--liquid-flow | Liquid Light | Brand upgrades, creative launches, fashion showcases | Fluid, dreamy, avant-garde | -| dark--premium-navy | Premium Navy & Gold | High-end corporate, annual strategy, board presentations | Authoritative, refined, premium | -| dark--investor-pitch | Investor Pitch Pro | Investor pitches, fundraising decks, business plans | Professional, trustworthy, composed | -| dark--cosmic-neon | Cosmic Neon | Science talks, futuristic topics, physics, cosmic themes | Sci-fi, mysterious, futuristic, neon | -| dark--editorial-story | Editorial Magazine Story | Brand storytelling, editorial magazines, content releases | Narrative, artistic, premium | -| dark--tech-cosmos | Tech Cosmos | Tech talks, architecture reviews, scientific presentations | Futuristic, scientific, cosmic | -| dark--blueprint-grid | Blueprint Grid | Technical planning, engineering blueprints, system architecture | Precise, professional, engineered | -| dark--diagonal-cut | Diagonal Industrial Cut | Industrial, engineering, construction, manufacturing | Rugged, powerful, bold | -| dark--spotlight-stage | Spotlight Stage | Keynotes, launch events, TED-style talks, galas | Dramatic, focused, theatrical | -| dark--cyber-future | Cyber Future | Futuristic topics, tech vision, cyberpunk, AI/robotics | Futuristic, cyberpunk, immersive | -| dark--circle-digital | Dark Digital Agency | Digital marketing, creative agencies, tech companies | Modern, dark-cool, digital | -| dark--architectural-plan | Architectural Plan | Architectural design, business plans, real estate development | Professional, structured, architectural | -| dark--luxury-minimal | Luxury Minimal | Luxury brands, premium products, high-end corporate | Luxurious, minimalist, sophisticated | -| dark--space-odyssey | Space Odyssey | Space/astronomy, science education, exploration narratives | Cosmic, inspiring, epic, exploratory | -| dark--neon-productivity | Neon Productivity | Productivity talks, tech workshops, motivation, startups | Energetic, modern, vibrant | -| dark--midnight-blueprint | Midnight Blueprint | Architecture firms, professional services, luxury real estate | Sophisticated, architectural, premium | -| dark--sage-grain | Sage Grain | Creative agencies, boutique consultancies, organic brands | Organic, sophisticated, artisanal | -| dark--obsidian-amber | Obsidian Amber | Finance, investment, luxury services, premium consulting | Premium, sophisticated, powerful | -| dark--velvet-rose | Velvet Rose | Luxury brands, premium fashion, high-end retail | Luxurious, elegant, refined | -| dark--aurora-softedge | Aurora Softedge | Design portfolios, creative showcases, art galleries | Aurora-like, dreamy, artistic | +| Directory | Style Name | Primary hex (bg / fg / accent) | Best For | Mood | +| ------------------------ | ------------------------ | ------------------------------ | --------------------------------------------------------------- | --------------------------------------- | +| dark--liquid-flow | Liquid Light | `#0F0F2D / #6C63FF / #48E5C2` | Brand upgrades, creative launches, fashion showcases | Fluid, dreamy, avant-garde | +| dark--premium-navy | Premium Navy & Gold | `#0C1B33 / #C9A84C / #1E3A5F` | High-end corporate, annual strategy, board presentations | Authoritative, refined, premium | +| dark--investor-pitch | Investor Pitch Pro | `#1A1A2E / #0F3460 / #16213E` | Investor pitches, fundraising decks, business plans | Professional, trustworthy, composed | +| dark--cosmic-neon | Cosmic Neon | `#050510 / #8A2BE2 / #00FFFF` | Science talks, futuristic topics, physics, cosmic themes | Sci-fi, mysterious, futuristic, neon | +| dark--editorial-story | Editorial Magazine Story | `#FFFFFF / #2C3E50 / #E74C3C` | Brand storytelling, editorial magazines, content releases | Narrative, artistic, premium | +| dark--tech-cosmos | Tech Cosmos | `-` | Tech talks, architecture reviews, scientific presentations | Futuristic, scientific, cosmic | +| dark--blueprint-grid | Blueprint Grid | `#1B3A5C / #4A90D9 / #FFFFFF` | Technical planning, engineering blueprints, system architecture | Precise, professional, engineered | +| dark--diagonal-cut | Diagonal Industrial Cut | `#1A1A1A / #FF6600 / #FFCC00` | Industrial, engineering, construction, manufacturing | Rugged, powerful, bold | +| dark--spotlight-stage | Spotlight Stage | `#0A0A0A / #FFFFFF / #FFE0B2` | Keynotes, launch events, TED-style talks, galas | Dramatic, focused, theatrical | +| dark--cyber-future | Cyber Future | `#0B0C10 / #66FCF1 / #1F2833` | Futuristic topics, tech vision, cyberpunk, AI/robotics | Futuristic, cyberpunk, immersive | +| dark--circle-digital | Dark Digital Agency | `#0D0E11 / #171A20 / #22252E` | Digital marketing, creative agencies, tech companies | Modern, dark-cool, digital | +| dark--architectural-plan | Architectural Plan | `#FFFFFF / #18293B / #B5D5E3` | Architectural design, business plans, real estate development | Professional, structured, architectural | +| dark--luxury-minimal | Luxury Minimal | `#111111 / #D4AF37 / #FFFFFF` | Luxury brands, premium products, high-end corporate | Luxurious, minimalist, sophisticated | +| dark--space-odyssey | Space Odyssey | `#0A0E27 / #1E3A5F / #4A5FFF` | Space/astronomy, science education, exploration narratives | Cosmic, inspiring, epic, exploratory | +| dark--neon-productivity | Neon Productivity | `#0B0F1A / #2BE4A8 / #FFB020` | Productivity talks, tech workshops, motivation, startups | Energetic, modern, vibrant | +| dark--midnight-blueprint | Midnight Blueprint | `#080B2A / #181B55 / #131650` | Architecture firms, professional services, luxury real estate | Sophisticated, architectural, premium | +| dark--sage-grain | Sage Grain | `#1E2720 / #FFFFFF / #D9B88F` | Creative agencies, boutique consultancies, organic brands | Organic, sophisticated, artisanal | +| dark--obsidian-amber | Obsidian Amber | `-` | Finance, investment, luxury services, premium consulting | Premium, sophisticated, powerful | +| dark--velvet-rose | Velvet Rose | `-` | Luxury brands, premium fashion, high-end retail | Luxurious, elegant, refined | +| dark--aurora-softedge | Aurora Softedge | `-` | Design portfolios, creative showcases, art galleries | Aurora-like, dreamy, artistic | ## Light Palette (light) -| Directory | Style Name | Best For | Mood | -| --------------------------- | ------------------------ | --------------------------------------------------------- | ----------------------------------- | -| light--minimal-corporate | Minimal Corporate Report | Annual reports, work summaries, business proposals | Professional, clean, composed | -| light--minimal-product | Minimal Product Showcase | Product launches, tech showcases, brand introductions | Modern, minimalist, premium | -| light--project-proposal | Project Proposal | Project kickoffs, business proposals, bid presentations | Professional, trustworthy, rigorous | -| light--bold-type | Bold Typography | Editorial layouts, magazine-style, brand manuals | Bold, modern, editorial | -| light--isometric-clean | Isometric Clean Tech | Tech products, SaaS platforms, data presentations | Fresh, modern, techy | -| light--spring-launch | Spring Launch Fresh | Spring launches, new product releases, seasonal marketing | Fresh, natural, vibrant | -| light--training-interactive | Interactive Training | Corporate training, online courses, knowledge sharing | Educational, interactive, friendly | -| light--watercolor-wash | Watercolor Wash | Art, cultural creative, tea ceremony, weddings | Soft, poetic, artistic | -| light--firmwise-saas | Firmwise SaaS | SaaS platforms, productivity tools, B2B software | Clean, efficient, trustworthy | -| light--glassmorphism-vc | Glassmorphism VC | VC funds, investment decks, fintech, startup pitches | Modern, premium, sophisticated | -| light--fluid-gradient | Fluid Gradient | AI/tech products, SaaS platforms, modern software | Fluid, tech-forward, dynamic | +| Directory | Style Name | Primary hex (bg / fg / accent) | Best For | Mood | +| --------------------------- | ------------------------ | ------------------------------ | --------------------------------------------------------- | ----------------------------------- | +| light--minimal-corporate | Minimal Corporate Report | `#FFFFFF / #E8EEF4 / #1E3A5F` | Annual reports, work summaries, business proposals | Professional, clean, composed | +| light--minimal-product | Minimal Product Showcase | `#FAFAFA / #00B894 / #2D3436` | Product launches, tech showcases, brand introductions | Modern, minimalist, premium | +| light--project-proposal | Project Proposal | `#E8EEF4 / #1E3A5F / #D4A84B` | Project kickoffs, business proposals, bid presentations | Professional, trustworthy, rigorous | +| light--bold-type | Bold Typography | `#F2F2F2 / #1A1A1A / #E8E8E8` | Editorial layouts, magazine-style, brand manuals | Bold, modern, editorial | +| light--isometric-clean | Isometric Clean Tech | `#F0F4F8 / #E8ECF1 / #4A90D9` | Tech products, SaaS platforms, data presentations | Fresh, modern, techy | +| light--spring-launch | Spring Launch Fresh | `#E8F5E9 / #4CAF50 / #8BC34A` | Spring launches, new product releases, seasonal marketing | Fresh, natural, vibrant | +| light--training-interactive | Interactive Training | `#FFF9E6 / #FF6B6B / #4ECDC4` | Corporate training, online courses, knowledge sharing | Educational, interactive, friendly | +| light--watercolor-wash | Watercolor Wash | `#FFFDF7 / #7AADCF / #E8A87C` | Art, cultural creative, tea ceremony, weddings | Soft, poetic, artistic | +| light--firmwise-saas | Firmwise SaaS | `#EFF2F7 / #7B3FF2 / #FFFFFF` | SaaS platforms, productivity tools, B2B software | Clean, efficient, trustworthy | +| light--glassmorphism-vc | Glassmorphism VC | `-` | VC funds, investment decks, fintech, startup pitches | Modern, premium, sophisticated | +| light--fluid-gradient | Fluid Gradient | `-` | AI/tech products, SaaS platforms, modern software | Fluid, tech-forward, dynamic | ## Warm Palette (warm) -| Directory | Style Name | Best For | Mood | -| ------------------------ | ------------------ | ----------------------------------------------------------------- | -------------------------------- | -| warm--earth-organic | Earth & Sage | Eco-friendly, sustainability, organic brands | Warm, sincere, natural | -| warm--minimal-brand | Minimal Brand | Brand introductions, product launches, premium brand showcases | Warm, refined, minimalist | -| warm--brand-refresh | Brand Refresh | Brand launches, corporate image updates, creative proposals | Fashionable, colorful, modern | -| warm--creative-marketing | Creative Marketing | Marketing campaigns, ad creatives, poster-style PPTs | Bold, impactful, expressive | -| warm--playful-organic | Playful Organic | Lifestyle, pet/animal topics, children's education, storytelling | Warm, playful, friendly | -| warm--sunset-mosaic | Sunset Mosaic | Engineering, infrastructure, B2B corporate, construction | Professional, warm, grounded | -| warm--coral-culture | Coral Culture | Company culture decks, HR presentations, team showcases | Warm, cultural, human-centered | -| warm--monument-editorial | Monument Editorial | Architecture, luxury brands, editorial magazines, studio branding | Monumental, refined, typographic | -| warm--vital-bloom | Vital Bloom | Wellness apps, yoga studios, mindful living, organic brands | Organic, vibrant, healthy | -| warm--bloom-academy | Bloom Academy | Education, e-learning, children's content, playful branding | Playful, educational, friendly | +| Directory | Style Name | Primary hex (bg / fg / accent) | Best For | Mood | +| ------------------------ | ------------------ | ------------------------------ | ----------------------------------------------------------------- | -------------------------------- | +| warm--earth-organic | Earth & Sage | `#F5F0E8 / #8B6F47 / #A8C686` | Eco-friendly, sustainability, organic brands | Warm, sincere, natural | +| warm--minimal-brand | Minimal Brand | `-` | Brand introductions, product launches, premium brand showcases | Warm, refined, minimalist | +| warm--brand-refresh | Brand Refresh | `#F5F0E8 / #162040 / #1A6BFF` | Brand launches, corporate image updates, creative proposals | Fashionable, colorful, modern | +| warm--creative-marketing | Creative Marketing | `-` | Marketing campaigns, ad creatives, poster-style PPTs | Bold, impactful, expressive | +| warm--playful-organic | Playful Organic | `#FFF8E7 / #3D3B3C / #FFFFFF` | Lifestyle, pet/animal topics, children's education, storytelling | Warm, playful, friendly | +| warm--sunset-mosaic | Sunset Mosaic | `-` | Engineering, infrastructure, B2B corporate, construction | Professional, warm, grounded | +| warm--coral-culture | Coral Culture | `-` | Company culture decks, HR presentations, team showcases | Warm, cultural, human-centered | +| warm--monument-editorial | Monument Editorial | `-` | Architecture, luxury brands, editorial magazines, studio branding | Monumental, refined, typographic | +| warm--vital-bloom | Vital Bloom | `-` | Wellness apps, yoga studios, mindful living, organic brands | Organic, vibrant, healthy | +| warm--bloom-academy | Bloom Academy | `-` | Education, e-learning, children's content, playful branding | Playful, educational, friendly | ## Vivid Palette (vivid) -| Directory | Style Name | Best For | Mood | -| ------------------------ | ----------------------- | ----------------------------------------------------- | ------------------------------- | -| vivid--candy-stripe | Rainbow Candy Stripe | Event celebrations, holidays, children's education | Joyful, lively, rainbow | -| vivid--playful-marketing | Vibrant Youth Marketing | Marketing campaigns, new product promos, sales events | Youthful, energetic, passionate | -| vivid--energy-neon | Energy Neon | Conferences, energy summits, tech events, editorial | Energetic, impactful, modern | -| vivid--pink-editorial | Pink Editorial | Annual reports, data journalism, editorial showcases | Contemporary, editorial, bold | -| vivid--bauhaus-electric | Bauhaus Electric | Creative agencies, design studios, bold branding | Bold, energetic, electric | +| Directory | Style Name | Primary hex (bg / fg / accent) | Best For | Mood | +| ------------------------ | ----------------------- | ------------------------------ | ----------------------------------------------------- | ------------------------------- | +| vivid--candy-stripe | Rainbow Candy Stripe | `#FFFFFF / #FF5252 / #FF7B39` | Event celebrations, holidays, children's education | Joyful, lively, rainbow | +| vivid--playful-marketing | Vibrant Youth Marketing | `#FFFFFF / #FF6B6B / #4ECDC4` | Marketing campaigns, new product promos, sales events | Youthful, energetic, passionate | +| vivid--energy-neon | Energy Neon | `#E8E8E8 / #00FF41 / #111111` | Conferences, energy summits, tech events, editorial | Energetic, impactful, modern | +| vivid--pink-editorial | Pink Editorial | `#160B33 / #7B2D52 / #C85080` | Annual reports, data journalism, editorial showcases | Contemporary, editorial, bold | +| vivid--bauhaus-electric | Bauhaus Electric | `-` | Creative agencies, design studios, bold branding | Bold, energetic, electric | ## Black & White (bw) -| Directory | Style Name | Best For | Mood | -| ----------------- | ------------- | ------------------------------------------------------------ | ------------------------------ | -| bw--mono-line | Minimal Line | Minimalist corporate, academic reports, consulting proposals | Calm, restrained, professional | -| bw--swiss-bauhaus | Swiss Bauhaus | Design agencies, architecture firms, art exhibitions | Rational, rigorous, classic | -| bw--brutalist-raw | Brutalist Raw | Avant-garde art shows, experimental design, indie brands | Rebellious, rugged, impactful | -| bw--swiss-system | Swiss System | Corporate, finance, consulting, professional services | Clean, systematic, bold | +| Directory | Style Name | Primary hex (bg / fg / accent) | Best For | Mood | +| ----------------- | ------------- | ------------------------------ | ------------------------------------------------------------ | ------------------------------ | +| bw--mono-line | Minimal Line | `#FFFFFF / #1A1A1A / #C8C8C8` | Minimalist corporate, academic reports, consulting proposals | Calm, restrained, professional | +| bw--swiss-bauhaus | Swiss Bauhaus | `#E63322 / #1C1C1C / #F5F5F5` | Design agencies, architecture firms, art exhibitions | Rational, rigorous, classic | +| bw--brutalist-raw | Brutalist Raw | `#FFFFFF / #000000 / #FF0000` | Avant-garde art shows, experimental design, indie brands | Rebellious, rugged, impactful | +| bw--swiss-system | Swiss System | `#FFFFFF / #000000 / #FF0000` | Corporate, finance, consulting, professional services | Clean, systematic, bold | ## Mixed Palette (mixed) -| Directory | Style Name | Best For | Mood | -| --------------------------- | -------------------- | ------------------------------------------------------- | --------------------------------- | -| mixed--duotone-split | Duotone Split | Brand launches, architectural design, premium showcases | Bold, architectural, minimal | -| mixed--chromatic-aberration | Chromatic Aberration | Tech startups, AI platforms, creative technology | Futuristic, glitch, cyber | -| mixed--bauhaus-blocks | Bauhaus Color Block | Creative studios, design portfolios, branding agencies | Bold, modernist, geometric | -| mixed--spectral-grid | Spectral Grid | Creative tech, innovation showcases, design conferences | Vibrant, innovative, experimental | +| Directory | Style Name | Primary hex (bg / fg / accent) | Best For | Mood | +| --------------------------- | -------------------- | ------------------------------ | ------------------------------------------------------- | --------------------------------- | +| mixed--duotone-split | Duotone Split | `#FFFFFF / #2D3436 / #E17055` | Brand launches, architectural design, premium showcases | Bold, architectural, minimal | +| mixed--chromatic-aberration | Chromatic Aberration | `#050814 / #0A1030 / #00F5E4` | Tech startups, AI platforms, creative technology | Futuristic, glitch, cyber | +| mixed--bauhaus-blocks | Bauhaus Color Block | `#F0EBE0 / #1D5C38 / #F4C040` | Creative studios, design portfolios, branding agencies | Bold, modernist, geometric | +| mixed--spectral-grid | Spectral Grid | `-` | Creative tech, innovation showcases, design conferences | Vibrant, innovative, experimental | --- diff --git a/skills/officecli-academic-paper/SKILL.md b/skills/officecli-academic-paper/SKILL.md index 4a17c3eac..c7d3c52e9 100644 --- a/skills/officecli-academic-paper/SKILL.md +++ b/skills/officecli-academic-paper/SKILL.md @@ -1,192 +1,504 @@ --- -# officecli: v1.0.24 name: officecli-academic-paper -description: "Use this skill when the user wants to create an academic paper, research paper, white paper, technical report, policy brief, or any formally structured document with TOC, equations, footnotes, endnotes, or scholarly formatting. Trigger on: 'academic paper', 'research paper', 'white paper', 'technical report', 'policy brief', 'journal paper', 'scholarly document', 'paper with equations', 'paper with footnotes', 'paper with TOC', 'manuscript', 'conference paper'. Output is always a single .docx file." +description: "Use this skill to build academic-style .docx output: journal / conference / thesis chapters carrying formal citation style (APA, Chicago, IEEE, MLA), numbered equations, figure & table cross-references, footnotes/endnotes, bibliography, or multi-column journal layout. Trigger on: 'research paper', 'journal paper', 'conference paper', 'manuscript', 'thesis', 'APA', 'MLA', 'Chicago', 'IEEE two-column', 'bibliography', 'hanging indent', 'citation style', 'abstract + keywords', 'equation numbering', 'cross-reference', paper with footnotes/endnotes. Output is a single .docx." --- -# Academic Paper Skill +# OfficeCLI Academic Paper Skill -Create formally structured Word documents with Table of Contents, equations (LaTeX to OMML), footnotes/endnotes, bibliography, and scholarly formatting. Output is a single `.docx` file. This skill supersedes the docx creating.md Academic Paper recipe -- use THIS skill for any document requiring TOC + equations + footnotes + formal structure. +**This skill is a scene layer on top of `officecli-docx`.** Every docx hard rule — style architecture, heading hierarchy, shell quoting, `break=newPage` alias, belt-and-suspenders page breaks, live PAGE field, Delivery Gate, renderer quirks — is inherited, not re-taught. This file adds only what academic papers need on top: citation styles, equations, SEQ / PAGEREF cross-refs, multi-column journal layout, bibliography hanging indent, abstract/keywords/affiliation block. ---- +When the docx base rules cover it, the text here says `→ see docx v2 §X`. Read docx v2 first if you have not. + +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. -## BEFORE YOU START (CRITICAL) +## ⚠️ Help-First Rule -**Every time before using officecli, run this check:** +**This skill teaches what an academic paper requires, not every command flag.** When a prop name, enum value, or field instruction is uncertain, consult help BEFORE guessing. ```bash -if ! command -v officecli &> /dev/null; then - echo "Installing officecli..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - # Windows: irm https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.ps1 | iex -else - CURRENT=$(officecli --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - LATEST=$(curl -fsSL https://api.github.com/repos/iOfficeAI/OfficeCLI/releases/latest | grep '"tag_name"' | sed -E 's/.*"v?([0-9.]+)".*/\1/') - if [ "$CURRENT" != "$LATEST" ]; then - echo "Upgrading officecli $CURRENT -> $LATEST..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - else - echo "officecli $CURRENT is up to date" - fi -fi -officecli --version +officecli help docx # All docx elements +officecli help docx # Full schema (e.g. section, equation, field, footnote) +officecli help docx --json # Machine-readable ``` ---- +Help is pinned to the installed CLI version. **When this skill and help disagree, help wins.** Every `--prop X=` in this file has been grep-verified against `officecli help docx ` — if help adds / renames a prop in a later version, trust help. -## Use When +## Mental Model & Inheritance -- User wants to create a **research paper, journal manuscript, or conference paper** with academic structure (abstract, numbered sections, references) -- User wants a **white paper, policy brief, or technical report** with formal structure (executive summary, TOC, branded headers) -- User needs **math equations** (LaTeX to OMML) in a Word document -- User needs **footnotes, endnotes, or bibliography** with hanging indent -- User needs a **Table of Contents** that auto-updates in Word -- User mentions "APA", "MLA", "Chicago", or any citation style formatting -- User needs **multi-column abstract** or **mixed portrait/landscape** sections +**Inherits docx v2.** You should have read `skills/officecli-docx/SKILL.md` first. This skill assumes you know how to add paragraphs, set styles, build tables, insert images, manage TOC/footer/headers, force page breaks, and run the Delivery Gate. If any of those are unfamiliar, open a second session on docx v2 before continuing. -## Don't Use When +## Shell & Execution Discipline -- User wants a **general letter, memo, contract, or simple report** -- use docx skill -- User wants a **presentation** -- use pptx skill -- User wants a **spreadsheet or dashboard** -- use xlsx or data-dashboard skill -- User needs **LaTeX output** (.tex file) -- this skill produces .docx only -- User needs **graduate thesis with Roman numeral front-matter pagination** -- deferred to Phase 2 -- User needs **tracked changes or collaborative editing markup** -- use docx skill with raw-set +**Shell quoting, incremental execution, `$FILE` convention** → see docx v2 §Shell & Execution Discipline. The same rules apply here verbatim — quote `[N]` paths, single-quote any value containing `$` (including `$2.8B` in a body paragraph or `@` DOIs), never hand-write `\$ \t \n` in executable examples, one command at a time. Academic-paper examples below use `$FILE` as a shell variable (`FILE="thesis.docx"`). ---- +## What "academic" means here (identity) -## What This Skill Produces +An academic paper is a docx with a **scholarly layer** on top: verifiable citations, precise equations, cross-refs that stay in sync, a formatted reference list. The base docx rules still apply; academic adds six deltas: -A single `.docx` file with: +1. **Citation style is a contract.** APA / Chicago / IEEE / MLA each dictate author format, date placement, reference-list order, in-text marker shape. Pick one at the start; every later decision (hanging indent, footnote vs parenthetical, `[1]` vs `(Smith, 2024)`) follows. +2. **Equations are first-class content** — inline `oMath` inside prose, display `oMathPara` as standalone blocks, optionally numbered. +3. **Figures and tables auto-number.** `SEQ Figure` / `SEQ Table` fields count them; `PAGEREF` links "see Fig. 2" to its live page number. +4. **Bibliography uses hanging indent** (first line flush left, continuation lines indented). Not first-line indent. Not left indent alone. Hanging. +5. **Abstract / keywords / affiliation block** is a first-page three-piece, not a cover in the marketing sense. Block-style abstract, no first-line indent, no decoration. +6. **Multi-column layout** appears in IEEE / ACM / Nature / many journals: single-column abstract + two-column body. -| Component | Description | -|-----------|-------------| -| Cover / title block | Centered title, authors, affiliations | -| Table of Contents | Native Word TOC field (levels 1-3), updateable | -| Structured sections | Heading1/2/3 hierarchy with consistent styling | -| Equations | Display and inline OMML from LaTeX subset | -| Footnotes / endnotes | Inline references at correct paragraph positions | -| Bibliography | Hanging indent, per-citation-style formatting | -| Headers / footers | Page numbers, optional branding | -| Optional: watermark, charts, custom bordered blocks, cross-references | +### Reverse handoff — when to go BACK to docx ---- +Stay in **docx v2** for white papers, policy briefs, technical reports, HR templates — anything without a venue / citation style. Use **this skill** only when the document will carry at least TWO of: citation-style biblio, equations, SEQ/PAGEREF cross-refs, multi-column, abstract + keywords block. -## Core Concepts - -### Style-First Architecture (NON-NEGOTIABLE) - -Define ALL styles before adding ANY content. Skipping style definitions causes formatting failures in 100% of cases. Different Word versions define Heading1 as 14pt, 16pt, or 13pt -- explicit style setup eliminates this variance. - -### Font Size Hierarchy - -| Style | Size | Weight | spaceBefore | spaceAfter | -|-------|------|--------|-------------|------------| -| Cover title | **20pt** | bold | 72pt (≈1 inch) | 24pt | -| Heading1 | >= 18pt (20pt preferred) | bold | 360 (18pt) | 120 (6pt) | -| Heading2 | >= 14pt | bold | 360 (18pt) | 80 (4pt) | -| Heading3 | >= 12pt | bold + italic | 240 (12pt) | 80 (4pt) | -| Body (Normal) | 11-12pt | regular | per paper type | per paper type | -| Caption | 9-10pt | italic | -- | -- | -| FootnoteText | 9-10pt | regular | -- | -- | - -### Verified LaTeX Subset - -| Category | LaTeX | Notes | -|----------|-------|-------| -| Fractions | `\frac{a}{b}` | Nested supported | -| Sub/superscripts | `x_i`, `x^{n+1}` | Multi-char needs braces | -| Summation | `\sum_{n=1}^{\infty}` | Limits above/below in display | -| Integration | `\int_0^{\infty}`, `\oint` | Single, double, and contour | -| Products | `\prod_{i=1}^{n}` | | -| Limits | `\lim_{x \to 0}` | | -| Square roots | `\sqrt{x}`, `\sqrt[3]{x}` | nth-root supported | -| Greek letters | `\alpha` .. `\Omega` | Both cases | -| Nabla / partial | `\nabla`, `\partial` | | -| Accents | `\hat{x}`, `\bar{x}`, `\tilde{x}`, `\vec{x}`, `\dot{x}` | | -| Bold math | `\mathbf{x}` | For vectors | -| Aligned | `\begin{aligned}...\end{aligned}` | Multi-line systems | -| Matrices | `\begin{pmatrix}...\end{pmatrix}` | Also bmatrix, vmatrix | -| Angle brackets | `\langle`, `\rangle` | For bra-ket notation | -| Simple delimiters | `\left[...\right]` | ONLY when NO sub/super inside | -| **DO NOT USE** | `\left[...\right]` + subscript/superscript inside | Cast error crash | -| **DO NOT USE** | `\left(...\right)` + subscript/superscript inside | Same crash | -| **DO NOT USE** | `\mathcal{L}` | Invalid XML -- use `\mathit{L}` | - -### Footnote Behavior - -Footnotes are inline reference runs within the target paragraph. They do NOT create new paragraphs and do NOT shift paragraph indices. You can add footnotes in any order -- forward, reverse, or arbitrary. The old "reverse order" advice is obsolete as of v1.0.24. +## Workflow — 5 verbs ---- +1. **Read the venue spec.** APA 7 / Chicago 17 / IEEE / MLA 9 / journal-specific. Line spacing, font, citation shape, biblio sort order — everything downstream follows from this one decision. +2. **Plan the sections.** Abstract → keywords → introduction → methods → results → discussion → conclusion → references. Estimate heading count for TOC decision (3+ headings = add a TOC, see docx v2 §Table of Contents). +3. **Set styles up front.** Heading1 / Heading2 / Heading3 / Caption / AbstractTitle / Bibliography. Define all styles BEFORE any content (→ see docx v2 §Paragraphs and styles — same rule here, same failure mode if skipped). +4. **Build body in order.** Cover / title block → abstract → keywords → TOC (if needed) → body sections in reading order → figures / tables with SEQ captions → bibliography → footnotes are added last by paragraph path. +5. **QA — Delivery Gate.** Inherit docx v2 Gates 1-3, then add academic Gates 4-5 below. -## Hard Rules (H1–H8) +## Requirements (academic floor on top of docx v2) -The following rules are non-negotiable. Any violation constitutes a delivery failure. +Everything in docx v2 §Requirements for Outputs applies. On top of that, academic papers MUST meet these additional rules: -| Rule | Requirement | -|------|-------------| -| H1 | `officecli validate` passes — zero XML errors | -| H2 | Cover page present with ≥7 of 10 required elements (title, authors, affiliation, submission target, date, abstract excerpt, keywords, horizontal rule, contact, subtitle) | -| H3 | All body sections use continuous numbered headings (e.g., "1. Introduction", "2. Methods") — see Section C.3 | -| H4 | Abstract paragraph has NO `firstLineIndent` (block style) | -| H5 | Table of Contents (TOC) field present | -| H6 | Dynamic PAGE field in footer (not static text) | -| H7 | Heading hierarchy is consistent — no level skipping (H1 → H2 → H3, never H1 → H3) | -| **H8** | **References/Bibliography section REQUIRED.** Every academic paper must have a final section titled "References" or "Bibliography" containing at minimum 5 formatted citations with hanging indent. A document with inline citations and no reference list is a delivery failure. | +### Typography and spacing (venue-aware) ---- +- **Font.** Times New Roman 11-12pt body (default) or venue-specified (IEEE uses Times 10pt 2-col; APA allows Calibri 11pt). Same body font throughout; no decorative heading fonts. +- **Heading hierarchy.** H1 = 20pt bold, H2 = 14pt bold, H3 = 12pt bold italic, body = 11-12pt. (Same numbers as docx v2 — restated because academic papers never rely on Word defaults.) +- **Line spacing.** APA 7 = 2x (double). Chicago / IEEE / most journals = 1.5x. Never below 1.15x. Set on body paragraphs and on References. +- **Margins.** 1 inch (1440 twips) all sides unless the venue says otherwise (some journals require 1.25in left for binding — check the spec). -## Workflow Overview +### Abstract, bibliography, caption placement -### Phase 1: Analyze Input -Classify paper type (social science, physics/math, white paper). Look up the Feature Selection Table in creating.md Section A. Plan which sections to follow. +- **Abstract is block-style.** NO `firstLineIndent`. Use `spaceAfter=12pt` for paragraph separation. If `view issues` reports "body paragraph missing first-line indent" on an Abstract paragraph, it's a false positive — ignore. +- **Bibliography uses hanging indent.** Each entry is one paragraph with `indent=720 hangingIndent=720` (left indent 0.5", first-line reversed by same amount). First line flush left; wraps indent under author name. +- **Figure captions go BELOW the figure.** Table captions go ABOVE the table. This is the single rule most non-academics get wrong — APA, Chicago, IEEE, MLA all agree on it. +- **Citation round-trip.** Every in-text citation key must resolve to an entry in the reference list. Delivery Gate 4 verifies. +- **SEQ presence.** Any paper with numbered figures or tables must carry live `SEQ Figure` / `SEQ Table` fields (not hardcoded "Figure 1" text that drifts when you insert a new figure mid-document). Delivery Gate 5 verifies. -### Phase 2: Setup -Create document, set defaults + margins, define ALL styles upfront. Plan section breaks if multi-column or landscape is needed. +### Cover / first-page block -### Phase 3: Build -Add content in order: cover, TOC, abstract, body sections, equations, tables, footnotes, bibliography, headers/footers, watermark. +Academic covers differ from professional covers. Minimum elements: title (centered, 20-22pt bold), author(s), affiliation, submission target or journal, date, abstract, keywords. The "60% fill" rule from docx v2 §Visual delivery floor still applies — a three-line cover with half a page of whitespace is a fail. See §Abstract / keywords / affiliation block below for the first-page recipe. -### Phase 4: QA -Run verification loop: `validate`, `view outline`, `view issues`, `view text`. Fix and re-verify. +### Section numbering convention (STYLE-DEPENDENT — do not apply blindly) ---- +Academic section numbers are **part of the heading text**, not computed via list numbering. `officecli`'s `numId`/`listStyle` mechanism is fragile across Heading1 re-use, so hand-write the prefix. BUT the prefix shape varies by style — DO NOT use the same form for all four: -## Quick Reference: Key Warnings - -| Warning | Detail | -|---------|--------| -| `\left`/`\right` + sub/super | Crashes with cast error. Use plain `()`, `[]` -- OMML auto-sizes. | -| pbdr at style level | `add /styles --prop pbdr.all=...` is silently dropped. `set /styles/X --prop pbdr.all=...` is rejected. Always set borders per-paragraph after creation. | -| Section break +1 offset | Each section break inserts one empty paragraph into /body. Account for +1 index offset on all subsequent `p[N]` references. | -| Shell escaping for LaTeX | Double backslashes in bash: `--prop "formula=\\frac{a}{b}"`. Use heredoc for complex formulas. | -| Dollar sign `$` in text | Bash expands `$` as variable in double quotes. Use single quotes or `\$`. See creating.md D-10. | -| Batch JSON values | ALL values must be strings: `"true"` not `true`, `"24"` not `24`. | -| Batch intermittent failure | ~1-in-15 failure rate. Retry on error. Keep arrays to 10-15 max. | -| TOC displays blank in LibreOffice | TOC field renders as "Update field to see table of contents" in LibreOffice/PDF — this is normal OOXML behavior. In Microsoft Word: Ctrl+A → F9 to update all fields. For LibreOffice-only recipients: add static text TOC paragraphs after the field, or include a delivery note asking the user to open in Word and press F9. | -| `move` on oMathPara not reliable | `move` command does not reliably reposition equation paragraphs (oMathPara elements). Workaround: use `add /body --type equation` to create the equation at the target position, then `remove` the original. Do NOT use `move` on equations. | -| `pbdr.bottom` XML order bug (P3) | `set --prop pbdr.bottom=...` may generate `` with child elements in wrong order, causing `validate` to report a pBdr schema error. **Workaround:** use `raw-set` to write the full `` XML manually (see creating.md D-4b). This is a known CLI bug — P3, CLI team owns the fix. | +| Style | H1 format | H2 format | Example | +|---|---|---|---| +| **APA 7** | **UNNUMBERED centered bold** | Unnumbered left-aligned bold | `Introduction` / `Methods` (centered) | +| **Chicago** | `"N. Title"` left-aligned | `"N.M Title"` | `1. Introduction`, `2.1 Policy Formation` | +| **IEEE** | `"N. TITLE"` ALL CAPS + Roman numerals | `A. Subtitle` title case | `I. INTRODUCTION`, `II. RELATED WORK`, `A. Datasets` | +| **MLA 9** | Unnumbered left-aligned bold | Same | `Literature Review` (no prefix) | ---- +APA 7 L1 headings are **centered, bold, unnumbered**; L2 are flush-left bold; L3 flush-left bold italic; L4/L5 run-in. Do NOT prefix APA headings with `1. / 2.` — that is Chicago/IEEE convention. IEEE wants ALL CAPS with Roman numerals (`I. INTRODUCTION`); inside each section, use `A./B./C.` sub-headings (title case). Arabic-numbered body sections are Chicago-style only. -## Quick Start (Social Science Paper Skeleton) +**Exception for all four**: References / Bibliography / Works Cited / Acknowledgments are unnumbered regardless of style — omit the `N.` prefix. + +## Quick Start — minimal APA paper ```bash -officecli create paper.docx -officecli set paper.docx / --prop defaultFont="Times New Roman" -officecli set paper.docx '/section[1]' --prop marginTop=1440 --prop marginBottom=1440 --prop marginLeft=1440 --prop marginRight=1440 -officecli add paper.docx /styles --type style --prop id=Heading1 --prop name="Heading 1" --prop type=paragraph --prop font="Times New Roman" --prop size=20 --prop bold=true --prop spaceBefore=360 --prop spaceAfter=120 --prop keepNext=true -officecli add paper.docx /body --type toc --prop levels=1-3 --prop title="Table of Contents" -officecli add paper.docx /body --type paragraph --prop text="Introduction" --prop style=Heading1 -officecli add paper.docx /body --type paragraph --prop text="This paper examines..." --prop size=12 --prop lineSpacing=2x +FILE="paper.docx" +officecli create "$FILE" +officecli open "$FILE" +officecli set "$FILE" / --prop defaultFont="Times New Roman" +officecli add "$FILE" /body --type paragraph --prop text="Remote Work and Team Cohesion" --prop align=center --prop size=20pt --prop bold=true --prop spaceAfter=24pt +officecli add "$FILE" /body --type paragraph --prop text="Alice Chen" --prop align=center --prop size=12pt +officecli add "$FILE" /body --type paragraph --prop text="Department of Psychology, Stanford University" --prop align=center --prop size=11pt --prop spaceAfter=24pt +officecli add "$FILE" /body --type paragraph --prop text="Abstract" --prop align=center --prop size=14pt --prop bold=true --prop spaceBefore=12pt --prop spaceAfter=6pt +officecli add "$FILE" /body --type paragraph --prop text="This study examines remote-work adoption on team cohesion across 18 months..." --prop size=12pt --prop lineSpacing=2x --prop spaceAfter=12pt +officecli add "$FILE" /body --type paragraph --prop text="Keywords: remote work, team cohesion, psychological safety" --prop italic=true --prop size=11pt --prop spaceAfter=18pt +officecli add "$FILE" /body --type paragraph --prop text="1. Introduction" --prop style=Heading1 --prop size=20pt --prop bold=true --prop spaceBefore=18pt --prop spaceAfter=12pt +officecli add "$FILE" /body --type paragraph --prop text="Remote-work research (Smith, 2024) has expanded since 2020..." --prop size=12pt --prop lineSpacing=2x --prop firstLineIndent=720 +officecli add "$FILE" /body --type paragraph --prop text="References" --prop style=Heading1 --prop size=20pt --prop bold=true --prop spaceBefore=18pt --prop spaceAfter=12pt +officecli add "$FILE" /body --type paragraph --prop text="Smith, J. (2024). Remote work and cohesion. Journal of Applied Psychology, 109(3), 412-430." --prop size=12pt --prop lineSpacing=2x --prop indent=720 --prop hangingIndent=720 +officecli add "$FILE" / --type footer --prop type=default --prop align=center --prop size=10pt --prop field=page +officecli close "$FILE" +officecli validate "$FILE" ``` -Follow [creating.md](creating.md) for the full step-by-step guide. +Ten-line skeleton. Real papers grow by adding more body paragraphs, more bibliography entries (each with the same `indent=720 hangingIndent=720` pair), figures / tables with captions, and a TOC if there are 3+ Heading1s. The Quick Start validates clean; the sections below elaborate each dimension. ---- +## Citation style recipes + +Four mainstream families. Pick one at project start; every downstream decision follows. **Per-style decision table:** + +| Style | In-text shape | Reference list order | Body line spacing | Footnotes? | +|---|---|---|---|---| +| APA 7 | `(Smith, 2024)` or `Smith (2024)` | Alphabetical by author | 2x (double) | Rare (content notes only) | +| Chicago 17 (Notes-Bib) | Superscript footnote number | Alphabetical by author | 1.5x-2x | **Primary** (full citation in footnote) | +| IEEE | `[1]`, `[2]`, ..., `[N]` | Order of first citation | 1.15x-1.5x, 2-col | Rare | +| MLA 9 | `(Smith 412)` page-number | Alphabetical by author, "Works Cited" | 2x | Rare | + +Shared defaults across all four: reference-list paragraphs use `indent=720 hangingIndent=720` (hanging indent 0.5"); add a live TOC if 3+ Heading1s (→ see docx v2 §Table of Contents); static TOC fallback if recipient cannot recalculate (→ see docx v2 §Report-level recipes (f)). + +### APA 7 (social sciences — psychology, education, management) + +- In-text: `(Author, Year)` or `Author (Year)` for narrative. Page number required on direct quotes: `(Smith, 2024, p. 15)`. Three+ authors: `(Smith et al., 2024)` after first citation. +- Reference list order: **alphabetical by first author's surname**. Title caps: sentence case for article titles, title case for journal names (italic). +- Reference shape: `Author, A. A., & Co-Author, B. B. (Year). Title of article. Journal Name, Volume(Issue), pages.` DOI preferred over URL; present as https URL, not `doi:` prefix. +- Double-space everything (`lineSpacing=2x`) including abstract and references. Body first-line indent = 0.5" (`firstLineIndent=720`). + +```bash +# Body paragraph with parenthetical citation +officecli add "$FILE" /body --type paragraph --prop text="Remote work adoption accelerated during the pandemic (Kramer & Kramer, 2020)." --prop size=12pt --prop lineSpacing=2x --prop firstLineIndent=720 +# Reference entry with hanging indent +officecli add "$FILE" /body --type paragraph --prop text="Kramer, A., & Kramer, K. Z. (2020). The potential impact of the Covid-19 pandemic on occupational status. Journal of Vocational Behavior, 119, 103442." --prop size=12pt --prop lineSpacing=2x --prop indent=720 --prop hangingIndent=720 +# DOI hyperlink appended to the reference paragraph +officecli add "$FILE" "/body/p[last()]" --type hyperlink --prop url="https://doi.org/10.1016/j.jvb.2020.103442" --prop text="https://doi.org/10.1016/j.jvb.2020.103442" +``` + +QA: `officecli query "$FILE" 'paragraph[hangingIndent]'` returns every reference entry; zero references with first-line indent instead of hanging. + +### Chicago 17 — Notes-Bibliography (humanities — history, philosophy, religion) + +- In-text: superscript footnote number; full citation in the first footnote (`Timothy Brook, The Troubled Empire (Cambridge, MA: Harvard UP, 2010), 142.`); **shortened form** thereafter (`Brook, Troubled Empire, 150.`). +- **Repeat-citation rule (Chicago 17, op. cit. deprecated):** + - **Immediately-consecutive** citation of **the same source, same page** → `Ibid.` + - **Immediately-consecutive, different page** of same source → `Ibid., 22.` + - Non-consecutive repeat → **shortened form** (`Brook, Troubled Empire, 150.`), NOT `op. cit.`. Chicago 17 drops `op. cit.` — use shortened form every time except for immediate repeats. +- Bibliography at end, **alphabetical by first author's surname** ("Brook, Timothy."), hanging indent. Footnote body renders at the viewer's footnote default (typically 10pt); bibliography entries 12pt. (The `footnote` element exposes only `text` — size is not settable per-footnote; trust renderer defaults.) +- Typical split for primary-source-heavy papers: `Primary Sources` and `Secondary Sources` as two Heading2s under a single `Bibliography` Heading1. Book titles italic in both footnotes and bibliography. +- Chicago also has an Author-Date variant used in the sciences — if the venue specifies Chicago Author-Date, fall back to the APA recipe and change only the punctuation (no comma between author and year: `(Smith 2024)`). + +```bash +# Body paragraph that will anchor a footnote, then the footnote itself +officecli add "$FILE" /body --type paragraph --prop text="The Ming dynasty's 海禁 policy shaped coastal trade for two centuries." --prop size=12pt --prop lineSpacing=1.5x --prop firstLineIndent=720 +officecli add "$FILE" "/body/p[last()]" --type footnote --prop text="Timothy Brook, The Troubled Empire: China in the Yuan and Ming Dynasties (Cambridge, MA: Harvard University Press, 2010), 142." +# Next footnote — shortened form +officecli add "$FILE" "/body/p[last()]" --type footnote --prop text="Brook, Troubled Empire, 150." +# Bibliography section split — primary sources first +officecli add "$FILE" /body --type paragraph --prop text="Bibliography" --prop style=Heading1 --prop size=20pt --prop bold=true --prop spaceBefore=18pt +officecli add "$FILE" /body --type paragraph --prop text="Primary Sources" --prop style=Heading2 --prop size=14pt --prop bold=true --prop spaceBefore=12pt +officecli add "$FILE" /body --type paragraph --prop text="Ming Shilu 明實錄. Taipei: Academia Sinica, 1966." --prop size=12pt --prop indent=720 --prop hangingIndent=720 +officecli add "$FILE" /body --type paragraph --prop text="Secondary Sources" --prop style=Heading2 --prop size=14pt --prop bold=true --prop spaceBefore=12pt +officecli add "$FILE" /body --type paragraph --prop text="Brook, Timothy. The Troubled Empire: China in the Yuan and Ming Dynasties. Cambridge, MA: Harvard University Press, 2010." --prop size=12pt --prop indent=720 --prop hangingIndent=720 +``` + +QA: `officecli query "$FILE" 'footnote'` count ≥ body-paragraph citation count. + +### IEEE (engineering — transactions, conference proceedings) + +- In-text: `[1]`, `[2]`. Numbered in **order of first appearance**, not alphabetical. Reuse the same number for repeat citations. `[1, p. 15]` for page refs, `[1]-[3]` for a range. +- Reference entry starts with the bracketed number: `[1] A. Smith and B. Jones, "Title," IEEE Trans. X, vol. 5, no. 3, pp. 1-10, 2024, doi: ...`. Authors are initial-first; journal names abbreviated per IEEE list (`IEEE Trans. Neural Netw.`, not full name). +- Body is **two-column** (see §Multi-column below). Abstract is single-column above the fold, 10pt, 1.15x line spacing, typically 200-250 words. +- First-line indent on body paragraphs = 0.2" (`firstLineIndent=288` twips ≈ 14pt). Smaller than APA's 0.5" because the 2-col width is narrower. +- **Section headings: ALL CAPS with Roman numerals** — `I. INTRODUCTION`, `II. RELATED WORK`, `III. METHOD`. Sub-sections `A. Datasets`, `B. Baselines` in title case. Do NOT use `1. Introduction` (Arabic) for IEEE — that is Chicago style. +- **Tables are numbered Roman**: `Table I`, `Table II`, `Table III`. Figures remain Arabic (`Fig. 1`, `Fig. 2`). The `SEQ Table` field emits Arabic cached values — for IEEE, patch the cached `` to Roman manually (see §SEQ cached-value trap), or accept Arabic and note in the cover letter. + +```bash +# Body citing reference 1 +officecli add "$FILE" /body --type paragraph --prop text="Attention-based anomaly detection has been applied to industrial sensor data [1], [2]." --prop size=10pt --prop lineSpacing=1.15x +# Reference list entry — number in the text +officecli add "$FILE" /body --type paragraph --prop text="[1] A. Smith and B. Jones, \"Attention for anomaly detection,\" IEEE Trans. Neural Netw., vol. 35, no. 2, pp. 412-430, 2024." --prop size=10pt --prop indent=720 --prop hangingIndent=720 +officecli add "$FILE" /body --type paragraph --prop text="[2] C. Lee, \"Time-series anomaly survey,\" in Proc. ICML, 2023, pp. 1200-1215." --prop size=10pt --prop indent=720 --prop hangingIndent=720 +``` + +QA: the highest `[N]` in body must equal the number of reference-list entries. Grep: `officecli view "$FILE" text | grep -oE '\[[0-9]+\]' | sort -u | tail -5`. + +### MLA 9 (literature, languages, cultural studies) + +Diff vs APA: in-text is `(Author Page)` **no comma** (e.g. `(Smith 412)`); direct quotes always carry the page number. Reference section titled **Works Cited** (not References / Bibliography). Entries alphabetical by surname, hanging indent, 2x spacing, nine "core elements" separated by periods: `Author. Title. Container, Other Contributors, Version, Number, Publisher, Date, Location.` — skip any that don't apply. Book titles italic; article titles in quotes. Otherwise identical to APA paragraph setup. + +## Equations (OMML — inline vs display) + +`--type equation` parses a LaTeX-ish formula into OMML. Two modes, selected by `--prop mode=`: + +| Mode | XML | Visual | Use | +|---|---|---|---| +| `display` (default) | `` at `/body` | Standalone centered block | Numbered equations, theorem statements | +| `inline` | `` appended to a run inside a paragraph | Runs with the text | `if $x > 0$` style in prose | + +```bash +# Display equation (own paragraph, centered) — explicitly set mode=display for clarity +officecli add "$FILE" /body --type equation --prop mode=display --prop formula="x^2 + y^2 = z^2" +# Display equation with Greek / subscript / integral — verify rendering below +officecli add "$FILE" /body --type equation --prop mode=display --prop formula="\\lambda_1 + \\alpha" +officecli add "$FILE" /body --type equation --prop mode=display --prop formula="\\frac{1}{2\\pi} \\int_0^{\\infty} e^{-x^2} dx" +# Inline equation INSIDE prose — required whenever variables like x_{t+1}, \lambda, etc. appear in a body paragraph: +officecli add "$FILE" /body --type paragraph --prop text="Given the weight " --prop size=11pt +officecli add "$FILE" "/body/p[last()]" --type equation --prop mode=inline --prop formula="W_t" +officecli add "$FILE" "/body/p[last()]" --type run --prop text=" we define the loss..." +``` + +**Verify equations render as OMML math**, not plain-text LaTeX tokens. After `close`, run: +```bash +officecli view "$FILE" text | head -20 # λ₁ + α, ∫₀∞, x² must appear as unicode math (verified renders) +officecli raw "$FILE" /document | grep -c '` child (illegal OOXML). Target `tc[N]/p[1]` with `mode=inline` if you need equations in cells. + +Full equation schema: `officecli help docx equation`. + +## Figures, tables, and cross-references (SEQ + PAGEREF) + +Two primitives, both **native fieldTypes** (verified against `officecli help docx field` v1.0.63): `seq` for auto-numbered caption counters, `pageref` for "see Fig. 2 on page 7" back-references. Native fields insert correctly, but their **cached rendered values** need a one-shot raw-set patch per field (see §SEQ cached-value trap below) — otherwise downstream viewers that don't recompute cached fields will show every figure as "Fig. 1". + +### SEQ auto-numbering — figures and tables + +A SEQ field is a counter with a name (`identifier`). Every `SEQ Figure` increments the Figure counter on **recalc**; every `SEQ Table` increments the Table counter. + +**⚠️ SEQ cached-value trap (verified on v1.0.63).** The CLI emits every SEQ field with cached result `1` — so a document with 3 Figure captions readbacks as `Figure 1 / Figure 1 / Figure 1` via `view text` or `query field[fieldType=seq]`, and any downstream viewer that doesn't recompute cached fields will display the same `Figure 1 / Figure 1 / Figure 1`. Word and WPS recompute on open when `w:updateFields=true` is set in settings. **Two must-do steps per paper with multiple figures/tables:** + +1. Flip `updateFields=true` in settings once per document (right after `create`). **Position matters** — OOXML `CT_Settings` schema rejects `` as the first child; insert it *before* ``: + ```bash + officecli raw-set "$FILE" /settings --xpath '//w:compat' --action insertbefore \ + --xml '' + ``` +2. **Patch the cached `` after each SEQ field** so the artifact reads correctly in every viewer: + ```bash + # After adding the Nth SEQ Figure caption, override cached "1" to the real number N: + officecli raw-set "$FILE" /document \ + --xpath "(//w:p[.//w:instrText[contains(text(),'SEQ Figure')]])[N]//w:fldChar[@w:fldCharType='separate']/following::w:t[1]" \ + --action replace \ + --xml 'N' + ``` + Repeat for N = 1, 2, 3, ... for every figure; same pattern with `SEQ Table` for tables. After patching, `officecli view "$FILE" text` will show `Figure 1 / Figure 2 / Figure 3` — and downstream viewers will too. + +```bash +# Figure with caption BELOW the image. Caption = "Figure : title" + optional bookmark for cross-ref. +officecli add "$FILE" /body --type picture --prop src=arch.png --prop width=5in +officecli set "$FILE" "/body/p[last()]/r[last()]" --prop alt="Model architecture: attention over time-series sensors" +# Caption paragraph (below the figure, per academic convention) +officecli add "$FILE" /body --type paragraph --prop text="Figure " --prop style=Caption --prop size=10pt --prop italic=true --prop align=center +officecli add "$FILE" "/body/p[last()]" --type field --prop fieldType=seq --prop identifier=Figure +officecli add "$FILE" "/body/p[last()]" --type run --prop text=": Attention-based anomaly detection model." +# Bookmark the caption so other paragraphs can PAGEREF it +officecli add "$FILE" /body --type bookmark --prop name=fig_arch +# Patch cached value — this is Figure 1 (first SEQ Figure in doc) +officecli raw-set "$FILE" /document \ + --xpath "(//w:p[.//w:instrText[contains(text(),'SEQ Figure')]])[1]//w:fldChar[@w:fldCharType='separate']/following::w:t[1]" \ + --action replace --xml '1' +``` + +### PAGEREF — cross-reference by bookmark + +```bash +# Cross-ref paragraph: "see Figure 1 on page X" +officecli add "$FILE" /body --type paragraph --prop text="As shown in Figure 1 (see page " --prop size=11pt --prop lineSpacing=1.5x +officecli add "$FILE" "/body/p[last()]" --type field --prop fieldType=pageref --prop name=fig_arch +officecli add "$FILE" "/body/p[last()]" --type run --prop text=")." +``` + +### Tables — caption ABOVE + +```bash +# Caption first (ABOVE the table), THEN the table +officecli add "$FILE" /body --type paragraph --prop text="Table " --prop style=Caption --prop size=10pt --prop italic=true --prop spaceAfter=6pt +officecli add "$FILE" "/body/p[last()]" --type field --prop fieldType=seq --prop identifier=Table +officecli add "$FILE" "/body/p[last()]" --type run --prop text=": Participant demographics (N=47)." +officecli add "$FILE" /body --type table --prop rows=5 --prop cols=4 --prop width=100% +# ... fill header + rows per docx v2 §Tables +``` + +### Verify SEQ + PAGEREF fields landed + +```bash +# At least one SEQ Figure or SEQ Table in the body document part +officecli raw "$FILE" /document | grep -c 'w:instrText[^>]*>[^<]*SEQ' # expect ≥ 1 +officecli raw "$FILE" /document | grep -c 'w:instrText[^>]*>[^<]*PAGEREF' # 0 ok if no cross-refs +``` + +Live fields carry **cached values** that render stale until a human presses F9 in Word. Expect "Figure 1" to show as `1`, `2`, ... immediately after recalc; before recalc, some viewers render `0` or blank. Judge field presence by `fldChar` existence, not by visible digit (→ see docx v2 §Field / cached-value spot-check). + +## Footnotes vs endnotes + +**Footnote** — sits at the bottom of the page where its anchor paragraph lives. Used for source citations in Chicago Notes-Bib, content asides in any style. + +**Endnote** — sits at the end of the document (or before the bibliography). Used by some venues in place of footnotes, or for long contextual notes that would clutter the page. + +```bash +# Footnote anchored to paragraph N +officecli add "$FILE" "/body/p[3]" --type footnote --prop text="Smith et al. reported similar findings in their 2023 review." +# Endnote +officecli add "$FILE" /endnotes --type endnote --prop text="Extended derivation of equation (4) is available at the project repository." +``` + +Both appear as empty-string runs in `view annotated` output (`r[N] ""`) — the run carries a `` XML element, not visible text. Confirm insertion with `officecli query "$FILE" 'footnote'` or `officecli get "$FILE" "/footnotes/footnote[N]"`. Footnotes do NOT shift paragraph indices; add them in any order after body content is in place. Full schema: `officecli help docx footnote` / `officecli help docx endnote`. + +## Bibliography section + +Every academic paper ends with a reference list. The name of the section depends on the style (**References** for APA / IEEE / Chicago Author-Date; **Bibliography** for Chicago Notes-Bib; **Works Cited** for MLA). Each entry is a separate paragraph with **hanging indent**. + +```bash +# Section heading — same as body Heading1 (excluded from body numbering by convention) +officecli add "$FILE" /body --type paragraph --prop text="References" --prop style=Heading1 --prop size=20pt --prop bold=true --prop spaceBefore=18pt --prop spaceAfter=12pt +# Each entry: hanging indent 720 twips (0.5"), with indent=720 as the partner (first line flush, wraps indented) +officecli add "$FILE" /body --type paragraph --prop text="Smith, J. (2024). Remote work and cohesion. Journal of Applied Psychology, 109(3), 412-430." --prop size=12pt --prop lineSpacing=2x --prop indent=720 --prop hangingIndent=720 +# DOI hyperlink on its own run appended to the entry paragraph +officecli add "$FILE" "/body/p[last()]" --type hyperlink --prop url="https://doi.org/10.1037/apl0001123" --prop text="https://doi.org/10.1037/apl0001123" +``` + +Verified: `--prop indent=720 --prop hangingIndent=720` is the canonical hanging-indent pair per `officecli help docx paragraph`. The old `ind.firstLine=-720` form (negative first-line indent) is NOT canonical and fails schema on emit — → see docx v2 §Schema-invalid-on-emit. + +**Round-trip QA.** Count in-text citation markers (APA `(Author, Year)`, IEEE `[N]`, MLA `(Author N)`) vs reference-list entries. See Delivery Gate 4 below. Every cited key must resolve; every listed entry should be cited at least once. + +## Multi-column (IEEE journal two-column recipe) + +IEEE and many engineering / physics journals render body text in two columns with a single-column abstract above. The mechanism: a section break with `type=continuous` and `columns=2`, then another section break at the end to **revert** to single-column. + +**The reversion step is not optional.** Without it, the rest of the document — including references — renders as two columns. This is the single most common multi-column failure. + +```bash +FILE="ieee.docx" +officecli create "$FILE" +officecli open "$FILE" + +# 1. Title, authors, affiliation — single-column (the default first section) +officecli add "$FILE" /body --type paragraph --prop text="Attention-Based Anomaly Detection for Industrial Time Series" --prop align=center --prop size=18pt --prop bold=true --prop spaceAfter=12pt +officecli add "$FILE" /body --type paragraph --prop text="Alice Chen, Bob Martinez" --prop align=center --prop size=11pt +officecli add "$FILE" /body --type paragraph --prop text="Department of CS, Stanford University" --prop align=center --prop size=10pt --prop spaceAfter=18pt + +# 2. Abstract — still single-column, block-style +officecli add "$FILE" /body --type paragraph --prop text="Abstract" --prop align=center --prop size=12pt --prop bold=true --prop spaceAfter=6pt +officecli add "$FILE" /body --type paragraph --prop text="We present an attention-based model for detecting anomalies in industrial sensor time series..." --prop size=10pt --prop lineSpacing=1.15x --prop spaceAfter=12pt + +# 3. Section break + two-column from here on +# CRITICAL: `/section[last()]` is REJECTED on v1.0.63 (cast-error). Count sections first, use explicit /section[N]. +officecli add "$FILE" /body --type section --prop type=continuous +SECTION_COUNT=$(officecli query "$FILE" section --json | jq '.data.results | length') +# After the add, SECTION_COUNT should be 2 — [1] is pre-break, [2] is post-break (2-col body area). +officecli set "$FILE" "/section[2]" --prop columns=2 --prop columnSpace=1cm + +# 4. Body — IEEE wants Roman numerals + ALL CAPS section titles (P1.2). +officecli add "$FILE" /body --type paragraph --prop text="I. INTRODUCTION" --prop style=Heading1 --prop size=10pt --prop bold=true +officecli add "$FILE" /body --type paragraph --prop text="Industrial anomaly detection has been studied since [1]..." --prop size=10pt --prop lineSpacing=1.15x --prop firstLineIndent=360 + +# 5. At the end of 2-column body, ANOTHER section break + revert to single column for references / appendices +# (If you want references in 2-col too, skip step 5 — but most IEEE papers use 2-col for references as well.) +# officecli add "$FILE" /body --type section --prop type=continuous +# Then re-count and use the new explicit /section[N], NOT /section[last()]: +# officecli set "$FILE" "/section[3]" --prop columns=1 + +# 6. Footer, close, validate +officecli add "$FILE" / --type footer --prop type=default --prop align=center --prop size=9pt --prop field=page +officecli close "$FILE" +officecli validate "$FILE" +``` + +**Visual verify.** Run `officecli view "$FILE" html` and Read the returned HTML to audit the rendered output. The abstract must render as full-width and the introduction onward as two columns. If the abstract wraps into two narrow columns, the first section break landed before the abstract — move it. + +**Section index bookkeeping.** Each `add /body --type section` inserts one empty paragraph into `/body` (the section-break marker). All subsequent `p[N]` indices shift by +1 per section break. Plan section breaks in advance; after adding a break, `officecli get "$FILE" /body --depth 1` to re-index before continuing. + +Full section schema (`columns`, `columnSpace`, `orientation`, `pageNumFmt`, `titlePage`, `lineNumbers`): `officecli help docx section`. + +## Abstract / keywords / affiliation block + +First-page metadata stack: title (centered 20-22pt bold) → authors (centered 12pt, superscript `^1 ^2` for multi-affiliation) → affiliations (centered 11pt, keyed to superscripts) → submission target / date → **Abstract** heading (14pt bold) → abstract body (block-style, **NO `firstLineIndent`**, 150-300 words) → keywords line (italic 11pt). Same "cover ≥ 60% filled" rule as docx v2. + +```bash +# Superscript affiliation markers (multi-institution paper) +officecli add "$FILE" /body --type paragraph --prop text="Alice Chen" --prop align=center --prop size=12pt +officecli add "$FILE" "/body/p[last()]" --type run --prop text="1" --prop superscript=true +officecli add "$FILE" "/body/p[last()]" --type run --prop text=", Bob Martinez" +officecli add "$FILE" "/body/p[last()]" --type run --prop text="2" --prop superscript=true +# Running header (skip on cover via type=first empty header — see docx v2 §headers) +officecli add "$FILE" / --type header --prop type=default --prop align=right --prop size=9pt --prop text="Short Running Title" +``` + +**Nature-family 2-col abstract** is rare — if required, open a `section type=continuous columns=2` BEFORE the abstract heading; short abstracts (<100 words) leave ragged columns. **Mirrored odd/even headers** need `` in settings via `raw-set` — not exposed by high-level API on 1.0.63; deliver without mirroring or inject the flag manually. Full header schema: `officecli help docx header`. + +## QA — Delivery Gate (executable) + +**Assume there are problems. Your job is to find them.** First render is almost never correct. Run this block before declaring done. + +### Gates 1-3 — inherited from docx v2 + +→ see docx v2 §Delivery Gate. Schema validate, token leak grep, live PAGE field structure. Copy-paste the docx v2 gate block first. Every check must print its success message. + +### Gate 4 — citation round-trip + +Every in-text citation key should resolve to a bibliography entry. Count mismatches = REJECT. + +```bash +# IEEE example (bracketed numerics). Adjust regex for APA (Author, Year) or MLA (Author Page). +CITATIONS=$(officecli view "$FILE" text | grep -oE '\[[0-9]+\]' | sort -u | wc -l) +ENTRIES=$(officecli query "$FILE" 'paragraph[hangingIndent]' --json | jq '.data.results | length') +echo "In-text citation markers: $CITATIONS | Bibliography entries: $ENTRIES" +# REJECT when citations exceed entries (cites without references). Entries > citations is allowed by some venues. +[ "$CITATIONS" -le "$ENTRIES" ] && echo "Gate 4 OK" || { echo "REJECT Gate 4: $CITATIONS in-text markers but only $ENTRIES bibliography entries"; exit 1; } +``` + +### Gate 5a — SEQ presence + cached numbers distinct + +If the paper has any numbered figure or table, the body must carry live `SEQ` fields AND their cached values must show distinct ascending numbers (else `view text` and downstream viewers that don't recompute cached fields will show "Figure 1" for all). + +```bash +# Count SEQ fields via query (raw-grep collapses multi-matches on one XML line → undercounts). +SEQ_COUNT=$(officecli query "$FILE" 'field[fieldType=seq]' --json | jq '.data.results | length') +VISIBLE_FIG=$(officecli view "$FILE" text | grep -cE '(Figure|Table) [0-9]+') +if [ "$VISIBLE_FIG" -gt 0 ] && [ "$SEQ_COUNT" -eq 0 ]; then + echo "REJECT Gate 5a: $VISIBLE_FIG visible Figure/Table labels but 0 SEQ fields." + exit 1 +fi +# Cached values must be distinct (CLI emits "1" per field by default → all three would show "Figure 1"). +# After the raw-set patches in §SEQ, view text should show Figure 1 / Figure 2 / Figure 3: +DISTINCT=$(officecli view "$FILE" text | grep -oE '(Figure|Table) [0-9]+' | sort -u | wc -l) +[ "$SEQ_COUNT" -le "$DISTINCT" ] && echo "Gate 5a OK (SEQ=$SEQ_COUNT, distinct=$DISTINCT)" || { echo "REJECT Gate 5a: $SEQ_COUNT SEQ fields but only $DISTINCT distinct rendered labels — patch cached after each SEQ field"; exit 1; } +``` + +### Gate 5b — Visual audit via HTML preview (MANDATORY, not optional) + +Gates 1–5a catch schema, token leaks, live-field presence, citation counts. **They do NOT catch physical assembly defects** — scrambled page order, a duplicated Abstract mid-document, three figures all labeled "Fig. 1" despite SEQ field presence, equation variables rendering as plain-text LaTeX (`lambda_1`, `x_{t+1}`) instead of math. Do not skip — Gates 1–5a pass ≠ visual OK. + +Run `officecli view "$FILE" html` and Read the returned HTML path. For every page of the paper, answer: + +> (a) Are pages in logical academic sequence? (Title → Abstract → Keywords → Introduction → body → References — no forward jumps, no backward leaks.) +> (b) Does the Abstract appear exactly once, not duplicated mid-document? +> (c) Are Figure N / Table N labels distinct and ascending? (Fig. 1, Fig. 2, Fig. 3 — not all "Fig. 1". Same for tables.) +> (d) Do equations render as math? (Italicized variables, Greek letters like λ / α, proper integrals / fractions — NOT plain-text `lambda_1`, `x_{t+1}`, `\int`.) +> (e) For IEEE papers: are section titles ALL CAPS with Roman numerals (`I. INTRODUCTION`)? Are tables Roman (`Table I`, `Table II`)? +> (f) For APA papers: are Level-1 headings centered bold and unnumbered (not `1. Introduction`)? +> (g) Does every in-text "see Fig. N" / "see Table N" resolve to a figure/table that actually carries that number? +> (h) Heading hierarchy visually distinct (size + weight) across H1 / H2 / H3? + +Report every instance. If even one defect is present → REJECT; do not deliver until fixed. + +**Human preview (optional).** If you want the user to visually preview the paper, run `officecli watch "$FILE"` for a live preview the user can open at their own discretion, or have them open the `.docx` directly in Word / WPS / Pages. For final visual verification, open the file in the target viewer. + +### Honest limit + +`validate` catches schema errors, not academic-style errors. A document passes `validate` with APA citations in an IEEE paper, footnotes in a style that forbids them, or figures with hardcoded numbers that drift when a new figure is inserted. The gates above — especially Gate 4 (round-trip) and Gate 5 (SEQ presence) — are how you catch what validate cannot. + +## Known Issues & Pitfalls (academic-specific) + +→ Base pitfalls (shell escape, `\$ \t \n` literals, table cell formatting order, `pageBreakBefore` belt-and-suspenders, `shd.fill` / `ind.firstLine` schema-invalid forms, TOC cached values, watermark two-step): see docx v2 §Known Issues & Pitfalls. + +Academic-specific: + +- **`\left(...\right)` / `\left[...\right]` + sub/superscript crashes.** Cast error. Use plain `(`, `)`, `[`, `]` — OMML auto-sizes in display mode. +- **`\mathcal{L}` emits invalid OMML.** Use `\mathit{L}` or plain uppercase. `\mathbf`, `\mathit`, `\mathbb` work; `\mathcal` does not. +- **`move` on `/body/oMathPara[N]` not reliable.** Do not rely on `move` to reposition display equations. Workaround: `add` at the target position, `remove` the original. +- **Section break +1 paragraph offset.** Each `add /body --type section` inserts one empty paragraph into `/body`. All `p[N]` indices after the break shift by +1. Plan breaks; after any `add section`, `officecli get "$FILE" /body --depth 1` to re-index. +- **`/section[last()]` is REJECTED on v1.0.63** (cast-error, same family as pptx's `/slide[last()]`). Always resolve to an explicit `/section[N]`: + ```bash + SECTION_COUNT=$(officecli query "$FILE" section --json | jq '.data.results | length') + # then use /section[2], /section[3], ..., NEVER /section[last()] + ``` + Each `add /body --type section` increments the count. Re-query after every break. +- **Multi-column does NOT auto-revert.** After a `columns=2` section, you must add another section break and explicitly set `columns=1` on the new `/section[N]` (N = post-revert count) — otherwise the rest of the document, including references, renders as two columns. Verify with `officecli get "$FILE" "/section[N]"` for each N. +- **`--type equation` targeting a `tc[N]` path emits illegal OOXML.** Inside a table cell, target `tc[N]/p[1]` with `--prop mode=inline` instead. Display equations (`oMathPara`) are not legal as direct `` children. +- **Hanging-indent canonical form is `indent=720 hangingIndent=720`.** Not `ind.firstLine=-720`. The dotted form emits `` after `` and fails schema on emit. +- **Footnote reference runs show as empty strings in `view annotated`.** The `` XML element has no visible text on the reference side; the note body lives in `/footnotes/footnote[N]`. Confirm with `officecli query "$FILE" 'footnote'`, not by eyeballing `view text`. +- **Caption placement:** Table caption ABOVE the table; Figure caption BELOW the figure. Every major style (APA, Chicago, IEEE, MLA) agrees. Putting a Table caption below the table is an academic-style error, not a rendering issue — `validate` will not catch it. +- **TOC cached rendering / static fallback / shell-escape:** → see docx v2 §TOC delivery step, §Report-level recipes (f), §Shell escape. + +## Renderer quirks (cross-viewer) + +→ see docx v2 §Renderer quirks. PAGE / TOC cached values, OMML baseline shifts, scheme colors — all identical quirks apply to academic papers. Before calling an equation or a citation marker broken, open the file in the user's target viewer (Word, WPS, Pages) — if it renders correctly there, it is a viewer quirk, not a skill defect. -## References +## Help pointer -- [creating.md](creating.md) -- Complete academic paper creation guide -- [docx SKILL.md](../docx/SKILL.md) -- General docx reading, editing, and QA reference -- [docx creating.md](../docx/creating.md) -- General building blocks (paragraphs, tables, images, etc.) +When in doubt: `officecli help docx`, `officecli help docx `, `officecli help docx --json`. Help is the authoritative schema; this skill is the decision guide for academic deltas on top of docx v2. diff --git a/skills/officecli-academic-paper/creating.md b/skills/officecli-academic-paper/creating.md deleted file mode 100644 index 930467c76..000000000 --- a/skills/officecli-academic-paper/creating.md +++ /dev/null @@ -1,749 +0,0 @@ - - -# Creating an Academic Paper - -Complete guide for building a formally structured Word document with TOC, equations, footnotes, and scholarly formatting. Follow this step by step. For general building blocks (paragraphs, runs, tables, images, lists, charts), see [docx creating.md](../docx/creating.md). - ---- - -## Section A: Overview and Decision Logic - -### A.1 What You Will Build - -A single `.docx` file with: cover page, Table of Contents, structured sections with headings, optional equations/footnotes/endnotes, bibliography with hanging indent, and professional formatting throughout. - -Three non-negotiable principles: - -- **All styles defined BEFORE any content** -- never rely on Word defaults -- **TOC uses native Word field** -- updateable, not manually typed -- **Bibliography uses hanging indent paragraphs** -- each reference is a separate paragraph - -### A.2 Analyze the Input (MANDATORY FIRST STEP) - -Before writing any commands: - -1. **Classify the paper type** -- social science (APA), physics/math, white paper, or closest match -2. **List required features** -- TOC, equations, footnotes, multi-column, landscape, custom styles -3. **Look up the Feature Selection Table** (A.3) to confirm which sections to follow -4. **Estimate command count** -- 45-60 (simple), 55-70 (medium), 80-100 (complex) - -### A.3 Feature Selection Table - -> **This table is the authoritative navigation aid.** Use it to decide which sections of this guide to follow. Skip sections marked NO for your paper type. - -| Feature | Social Science (APA) | Physics/Math | CS / Engineering | Life Science / Biomedical | White Paper | Section | -|---------|:---:|:---:|:---:|:---:|:---:|---------| -| TOC | YES | YES | Optional | Optional | YES | C.2 | -| Equations (OMML) | NO | YES | Optional | NO | NO | D.1 | -| Footnotes | YES (endnotes) | YES | YES (see D.3) | NO | YES | D.3 | -| Multi-column abstract | NO | YES | NO | NO | NO | C.1 | -| Landscape sections | NO | YES (figures) | NO | YES (figures) | NO | B.3 | -| Section breaks | NO | YES | NO | YES (cover) | YES (cover) | B.3 | -| Custom styles (Theorem etc.) | NO | Optional | NO | NO | NO | B.2 | -| Paragraph borders | NO | Optional | NO | NO | NO | D.2 | -| Watermark | NO | NO | NO | NO | YES | E.2 | -| Charts | NO | NO | NO | NO | NO | docx creating.md | -| Cross-references (REF) | NO | NO | NO | NO | NO | E.3 | -| Header/footer branding | NO | NO | NO | NO | YES | E.1 | - -**CS / Engineering paper defaults:** body `lineSpacing=1.5x`, font `Times New Roman` or `Calibri` 11-12pt, references in numbered `[1]` format with `leftIndent=720 hangingIndent=720`, first-line indent optional (block style also acceptable). Use `spaceBefore=360` on Heading1 (same as Physics/Math). Footnotes are supported (see D.3); inline citations in `[N]` format are more common but footnotes may be used when the venue requires them. - -**Life Science / Biomedical paper defaults:** Single-column layout (no multi-column abstract). Body `lineSpacing=1.5x` or `2x` (varies by journal — Nature uses 1.5x, many others require double-spaced). Font `Times New Roman` or `Arial` 11-12pt. References in numbered `[N]` format (ACS/Nature/Vancouver style) with `leftIndent=720 hangingIndent=720`. Key structural additions: -- **Figure captions** are critical — use the Caption style below each image/placeholder (figure caption goes BELOW the figure, unlike table captions which go ABOVE) -- **Ethics statement** / IRB approval section: add as a separate Heading1 section if required by the journal -- **Author contributions** section (CRediT format): recommended for multi-author papers -- Use `spaceBefore=360` on Heading1 (same as other science types) - -> **STOP here and plan.** Before writing any commands, write out: -> 1. Which paper type? Which features from the table? -> 2. How many sections, headings, tables, equations, footnotes? -> 3. Will you need section breaks? If yes, plan the index offsets now. - ---- - -## Section B: Setup and Styles - -### B.1 Create and Configure - -```bash -# Create document -officecli create paper.docx - -# Open in resident mode — all subsequent operations run in memory -officecli open paper.docx - -# Set default font -officecli set paper.docx / --prop defaultFont="Times New Roman" - -# Set metadata -officecli set paper.docx / --prop title="Paper Title" --prop author="Author Name" - -# Set margins (1 inch = 1440 twips on all sides) -officecli set paper.docx '/section[1]' --prop marginTop=1440 --prop marginBottom=1440 --prop marginLeft=1440 --prop marginRight=1440 - -# ... all content operations (styles, headings, body, tables, equations) ... - -# Close and validate when done -officecli close paper.docx -officecli validate paper.docx -``` - -**Paper-type line spacing:** - -| Paper Type | Body lineSpacing | Font | Size | -|-----------|-----------------|------|------| -| APA / Social Science | `2x` (double) | Times New Roman | 12pt | -| Physics / Math | `1.5x` (recommended) | Times New Roman | 11pt | -| CS / Engineering | `1.5x` (recommended) | Times New Roman | 11-12pt | -| White Paper | `1.5x` recommended, `1.15x` minimum | Calibri | 11pt | - -> **Line spacing note:** Recommended body line spacing is **1.5x** for all academic paper types. `1.15x` passes validation but is below publication norm — use it only for white papers or internal reports where space is at a premium. For manuscripts submitted for peer review, use `1.5x` or `2x` (double). Never use line spacing below `1.15x`. - -### B.2 Define ALL Styles Upfront (NON-NEGOTIABLE) - -> **WARNING: Skipping style definitions causes formatting failures. Define ALL styles before adding ANY content. This is the #1 failure mode in document creation.** - -> **WARNING: Blank documents created with `officecli create` have NO styles part. You MUST use `add /styles --type style` to create heading styles -- `set /styles/Heading1` will FAIL on a blank document because the style does not exist yet.** - -```bash -# Create heading styles (add, NOT set -- blank documents have no built-in styles) -officecli add paper.docx /styles --type style --prop id=Heading1 --prop name="Heading 1" --prop type=paragraph --prop font="Times New Roman" --prop size=20 --prop bold=true --prop spaceBefore=360 --prop spaceAfter=120 --prop keepNext=true -officecli add paper.docx /styles --type style --prop id=Heading2 --prop name="Heading 2" --prop type=paragraph --prop font="Times New Roman" --prop size=14 --prop bold=true --prop spaceBefore=360 --prop spaceAfter=80 --prop keepNext=true -officecli add paper.docx /styles --type style --prop id=Heading3 --prop name="Heading 3" --prop type=paragraph --prop font="Times New Roman" --prop size=12 --prop bold=true --prop italic=true --prop spaceBefore=240 --prop spaceAfter=80 --prop keepNext=true - -# Create custom styles (only if needed per Feature Selection Table) -officecli add paper.docx /styles --type style --prop id=AbstractTitle --prop name="Abstract Title" --prop basedOn=Normal --prop font="Times New Roman" --prop size=14 --prop bold=true --prop alignment=center -# AbstractTitle is a separate style (not Heading1) to prevent the abstract from appearing -# in the TOC. If you use Heading1 for "Abstract", it will be listed in the Table of Contents -# alongside body sections. -officecli add paper.docx /styles --type style --prop id=Caption --prop name=Caption --prop basedOn=Normal --prop font="Times New Roman" --prop size=10 --prop italic=true - -# Physics/Math only: Theorem, Definition, Proof styles -officecli add paper.docx /styles --type style --prop id=Theorem --prop name=Theorem --prop basedOn=Normal --prop italic=true --prop font="Times New Roman" --prop size=11 --prop spaceBefore=240 --prop spaceAfter=120 -officecli add paper.docx /styles --type style --prop id=Definition --prop name=Definition --prop basedOn=Normal --prop font="Times New Roman" --prop size=11 --prop spaceBefore=240 --prop spaceAfter=120 -officecli add paper.docx /styles --type style --prop id=Proof --prop name=Proof --prop basedOn=Normal --prop italic=true --prop font="Times New Roman" --prop size=11 -``` - -### B.3 Section Break Strategy - -> **D-1: Section break inserts an empty paragraph.** After `add /body --type section`, one empty paragraph is added to `/body`. All subsequent `p[N]` indices shift by +1. - -**Before section break:** -``` -p[6] = "Methods text" -``` -**After `officecli add paper.docx /body --type section --prop type=continuous`:** -``` -p[6] = "Methods text" -p[7] = "" <-- empty paragraph (section break marker) -p[8] = next content <-- shifted +1 -``` - -**Plan all section breaks BEFORE building.** Count them and add their +1 offsets to your paragraph index plan. - -**Abstract paragraph style — firstLineIndent exemption:** - -> Abstract paragraphs use **block paragraph style** (no first-line indent). Do NOT set `firstLineIndent` on the abstract body paragraph. -> - Control spacing via `spaceBefore` / `spaceAfter` only -> - If `view issues` reports "body paragraph missing first-line indent" for the abstract paragraph, this is a **false positive** — ignore it. The abstract is intentionally block-formatted per academic convention. - -**Multi-column abstract** -- section break pair with columns=2, then explicit revert. For dual-column abstracts, ensure at least 150-200 words so both columns are filled evenly. Short abstracts (< 100 words) will appear only in the left column. - -> **WARNING: The final sectPr inherits the last section's properties. Adding a section break does NOT automatically revert columns to 1. You MUST explicitly set `columns=1` on the section after the multi-column zone, or the rest of the document (including body text, references, etc.) will render as 2-column.** - -```bash -officecli add paper.docx /body --type section --prop type=continuous # start 2-col zone -officecli set paper.docx '/section[N]' --prop columns=2 -# ... add abstract content here ... -officecli add paper.docx /body --type section --prop type=continuous # end 2-col zone -officecli set paper.docx '/section[N+1]' --prop columns=1 # REQUIRED: explicitly reset to 1 column -``` - -**Verification (REQUIRED after multi-column setup):** - -```bash -# Check columns on each section -- all sections after the abstract must show columns=1 -officecli get paper.docx '/section[1]' -officecli get paper.docx '/section[2]' -officecli get paper.docx '/section[3]' -# ... check every section. If any non-abstract section shows columns=2, fix with: -# officecli set paper.docx '/section[K]' --prop columns=1 -``` - -**Landscape section** -- for wide tables/figures: - -```bash -officecli add paper.docx /body --type section --prop type=nextPage --prop orientation=landscape -# ... add wide content ... -officecli add paper.docx /body --type section --prop type=nextPage --prop orientation=portrait -``` - ---- - -## Section C: Structure - -### C.1 Title Block / Cover Page - -> **Lesson 8/9 Reminder:** A cover page must be content-rich. Do NOT output a sparse cover with only 3 lines of text followed by a full page of whitespace — this is a visual failure. Aim for content occupying ≥ 60% of the cover page area. - -> **⚠️ Cover Density Warning:** If you only include title + authors + date, the cover will occupy approximately 30–40% of the page — far below the ≥60% requirement. You MUST supplement with an abstract excerpt AND/OR keywords AND/OR affiliation block to reach the density target. Every academic paper cover must include **at least 8 of the 10 elements** listed below. - -**Cover Page Required Elements (≥8 of 10 for ≥60% coverage):** - -| # | Element | Required | Notes | -|---|---------|:--------:|-------| -| 1 | Paper title | YES | Large font, **20–24pt**, bold, centered | -| 2 | Subtitle | If applicable | 14–16pt, centered, italic | -| 3 | Author name(s) — all authors listed | YES | 11–12pt, centered | -| 4 | Affiliation block (department, university, institution) | YES | 11–12pt, centered, each on its own line | -| 5 | Submitted to (journal or conference name) | YES | e.g. `"Submitted to: Nature Methods"` | -| 6 | Submission / preparation date | YES | e.g. `"April 2026"` | -| 7 | Abstract excerpt (4–8 lines, italic) | YES | First 3–5 sentences of the abstract; label with `"Abstract:"` prefix | -| 8 | Keywords (5–8 terms, italic) | YES | e.g. `"Keywords: machine learning, NLP, transformer, BERT, fine-tuning"` | -| 9 | Horizontal rule or decorative divider | Recommended | Use a full-width border paragraph to visually separate sections | -| 10 | Contact email / ORCID | Optional | e.g. `"Contact: alice@stanford.edu \| ORCID: 0000-0001-2345-6789"` | - -> **Minimum viable cover:** If the scenario is explicitly minimal, include at minimum: title + authors + affiliation + submission target + date + abstract excerpt + keywords (7 elements). Three lines alone is **never** acceptable. - -**Rich cover page example (recommended pattern):** - -```bash -# Title (large, bold, centered — 20-24pt) -officecli add paper.docx /body --type paragraph \ - --prop text="Deep Learning Approaches to Gene Expression Prediction" \ - --prop alignment=center --prop font="Times New Roman" --prop size=22 \ - --prop bold=true --prop spaceBefore=72pt --prop spaceAfter=18pt -# NOTE: spaceBefore=72pt uses point units (72pt ≈ 1 inch of top padding). -# Other style examples use raw twip values (e.g. spaceBefore=360 = 18pt). Both accepted. - -# Authors -officecli add paper.docx /body --type paragraph \ - --prop text="Alice Chen, Bob Martinez" \ - --prop alignment=center --prop font="Times New Roman" --prop size=12 --prop spaceAfter=6pt - -# Affiliation -officecli add paper.docx /body --type paragraph \ - --prop text="Department of Computer Science, Stanford University" \ - --prop alignment=center --prop font="Times New Roman" --prop size=12 --prop spaceAfter=6pt - -# Contact email -officecli add paper.docx /body --type paragraph \ - --prop text="Contact: alice.chen@stanford.edu" \ - --prop alignment=center --prop font="Times New Roman" --prop size=11 --prop spaceAfter=18pt - -# Advisor (if applicable) -officecli add paper.docx /body --type paragraph \ - --prop text="Advisor: Prof. Jane Smith" \ - --prop alignment=center --prop font="Times New Roman" --prop size=11 --prop spaceAfter=6pt - -# Submission info -officecli add paper.docx /body --type paragraph \ - --prop text="Submitted to: NeurIPS 2026 Workshop on Computational Biology" \ - --prop alignment=center --prop font="Times New Roman" --prop size=11 --prop spaceAfter=6pt - -# Date -officecli add paper.docx /body --type paragraph \ - --prop text="April 2026" \ - --prop alignment=center --prop font="Times New Roman" --prop size=11 --prop spaceAfter=24pt - -# Abstract excerpt (2-3 sentences to fill the cover — omit if content is already dense) -officecli add paper.docx /body --type paragraph \ - --prop text="Abstract: This paper presents a transformer-based architecture for predicting gene expression levels from DNA sequence. We achieve state-of-the-art performance on the ENCODE benchmark, demonstrating 12% improvement over prior methods." \ - --prop alignment=center --prop font="Times New Roman" --prop size=11 \ - --prop italic=true --prop spaceBefore=12pt --prop spaceAfter=18pt - -# Keywords -officecli add paper.docx /body --type paragraph \ - --prop text="Keywords: gene expression, deep learning, transformer, computational biology, DNA sequence" \ - --prop alignment=center --prop font="Times New Roman" --prop size=11 --prop spaceAfter=0pt - -officecli add paper.docx /body --type pagebreak # or section break for white paper -``` - -> **Minimum viable cover (simple papers):** If the paper is short or the scenario is minimal, you must still include at minimum: title + authors + affiliation + date + keywords. Three lines alone is never acceptable. - -### C.2 Table of Contents - -```bash -officecli add paper.docx /body --type toc --prop levels=1-3 --prop title="Table of Contents" -``` - -The TOC is a native Word field. It shows "Update Field" prompt in Word -- right-click and select "Update entire table" to populate. Add the TOC early in the document; it picks up all headings regardless of section breaks. - -> **⚠️ TOC LibreOffice 渲染说明:** TOC 字段在 LibreOffice 中以占位文本 "Update field to see table of contents" 显示。 -> 这是正常的 OOXML 行为——在 Microsoft Word 中打开后按 Ctrl+A → F9 更新所有字段即可显示完整目录。 -> 如果接收者仅使用 LibreOffice,可以考虑以下替代方案: -> 1. 在 TOC 字段之后手动添加目录段落(静态文本,每个标题一行),作为 LibreOffice 可见的目录 -> 2. 或在文档开头的覆信中说明"请在 Word 中更新目录字段(F9)" - -Add a page break after the TOC to separate it from body content: - -```bash -officecli add paper.docx /body --type pagebreak -``` - -### C.3 Body Sections with Headings - -**Section Numbering Convention:** - -Standard academic papers include section numbers directly in the heading text. Use the following format — the number is part of the `text` property, **not** implemented via `numbering` or `listStyle`: - -| Level | Format | Example | -|-------|--------|---------| -| H1 (主章节) | `"N. Title"` | `"1. Introduction"`, `"2. Methods"`, `"3. Results"` | -| H2 (子章节) | `"N.M Title"` | `"2.1 Data Collection"`, `"2.2 Analysis"` | -| H3 (三级) | `"N.M.K Title"` | `"2.1.1 Preprocessing"`, `"2.1.2 Normalization"` | - -> **Continuous numbering — ALL body sections, no exceptions:** ALL body sections including Conclusion, Discussion, and Policy Recommendations MUST follow the numbered heading convention (e.g., `"5. Conclusion"`, `"6. Policy Recommendations"`). Do NOT leave any content body section unnumbered. Only **References/Bibliography** and **Acknowledgments** are exempt from numbering by academic convention. - -```bash -# H1 with number in text (correct approach) -officecli add paper.docx /body --type paragraph --prop text="1. Introduction" --prop style=Heading1 -officecli add paper.docx /body --type paragraph --prop text="2. Methods" --prop style=Heading1 - -# H2 with dotted sub-number -officecli add paper.docx /body --type paragraph --prop text="2.1 Data Collection" --prop style=Heading2 -officecli add paper.docx /body --type paragraph --prop text="2.2 Statistical Analysis" --prop style=Heading2 - -# H3 three-level -officecli add paper.docx /body --type paragraph --prop text="2.1.1 Preprocessing Steps" --prop style=Heading3 -``` - -> **Do NOT use `numbering` or `listStyle` properties for section numbering.** Embed the number directly in the `text` value. This is simpler, more predictable, and avoids list numbering reset issues. - -**Body paragraphs:** - -```bash -# Body paragraph (adjust lineSpacing per paper type from B.1) -officecli add paper.docx /body --type paragraph --prop text="This paper examines..." --prop font="Times New Roman" --prop size=12 --prop lineSpacing=2x --prop spaceAfter=0pt --prop firstLineIndent=720 -``` - -For APA style: use `firstLineIndent=720` (0.5") on body paragraphs, `lineSpacing=2x`, no extra `spaceAfter`. - ---- - -## Section D: Content - -### D.1 Equations -- Display and Inline - -```bash -# Display equation (centered, own line) -officecli add paper.docx /body --type equation --prop "formula=E = mc^2" - -# Inline equation (within existing paragraph) -officecli add paper.docx '/body/p[N]' --type equation --prop "formula=x^2 + y^2 = r^2" --prop mode=inline - -# Complex equation via heredoc (avoids escaping) -officecli add paper.docx /body --type equation --prop formula="\frac{1}{2\pi} \int_{BZ} \Omega(\mathbf{k}) \, d^2\mathbf{k}" -``` - -**Shell escaping:** bash `--prop` = double backslash (`\\frac`), batch JSON = quadruple (`\\\\frac`), heredoc = double (`\\frac`). - -> **WARNING: Inline equations are appended to the end of the target paragraph, not inserted at a specific position within the text.** For best results, structure the paragraph so the equation naturally belongs at the end, or split the sentence into two paragraphs with the equation between them. - -> **D-2: `\left`/`\right` + subscript/superscript crashes.** Any `\left[...\right]` or `\left(...\right)` containing subscript or superscript content throws a cast error. Use plain `(`, `)`, `[`, `]` instead -- OMML auto-sizes delimiters in display mode. - -> **D-3: `\mathcal` causes validation error.** Use `\mathit{L}` or plain letters instead. - -See SKILL.md Core Concepts for the full verified LaTeX subset table. - -> **Known limitation: Equation numbering (e.g., "(1)", "(2)") is not natively supported.** There is no automatic equation numbering mechanism in the current version. The recommended workaround (Plan A below) uses two consecutive paragraphs — one centered for the equation, one right-aligned for the number. - -**Manual equation numbering — recommended implementation (Plan A):** - -```bash -# Step 1: display equation paragraph (centered) -officecli add paper.docx /body --type equation --prop "formula=O(n \log n)" -# The equation is added as its own centered paragraph by default. - -# Step 2: equation number paragraph immediately after (right-aligned, no top spacing) -officecli add paper.docx /body --type paragraph --prop text="(1)" \ - --prop alignment=right --prop size=11 --prop spaceBefore=0 --prop spaceAfter=6 -``` - -**Manual equation numbering — Plan B: 3-column table (recommended for precise alignment):** - -When equation and number must appear on the same baseline (e.g., journal submission requirements), use a borderless 1×3 table with column proportions 10% : 75% : 15%: - -```bash -# Step 1: Add a 1×3 table (left spacer | equation | number) -officecli add paper.docx /body --type table --prop rows=1 --prop cols=3 - -# Step 2: Set column widths (total page width = ~7920 twips for 1-inch margins) -# col1 (left spacer): ~792 twips (10%) -# col2 (equation): ~5940 twips (75%) -# col3 (number): ~1188 twips (15%) -officecli set paper.docx '/body/tbl[N]/tr[1]/tc[1]' --prop width=792 -officecli set paper.docx '/body/tbl[N]/tr[1]/tc[2]' --prop width=5940 -officecli set paper.docx '/body/tbl[N]/tr[1]/tc[3]' --prop width=1188 - -# Step 3: Add equation to center column -# CRITICAL: Do NOT use `--type equation` targeting a table cell path (tc) directly. -# That generates oMathPara as a direct w:tc child, which is illegal OOXML and causes validate errors. -# Use --prop mode=inline and target the paragraph inside the cell (tc/p[1]): -officecli add paper.docx '/body/tbl[N]/tr[1]/tc[2]/p[1]' --type equation --prop "formula=E = mc^2" --prop mode=inline - -# Step 4: Add number to right column, right-aligned -officecli set paper.docx '/body/tbl[N]/tr[1]/tc[3]' --prop alignment=right -officecli add paper.docx '/body/tbl[N]/tr[1]/tc[3]/p[1]' --type paragraph --prop text="(1)" --prop alignment=right --prop size=11 - -# Step 5: Hide table borders -officecli set paper.docx '/body/tbl[N]' --prop border.all="none" -``` - -Replace `tbl[N]` with the correct 1-based table index. For multi-equation papers, repeat Steps 1–5 for each equation and increment the number `(1)`, `(2)`, etc. - -> **Plan A vs Plan B:** Plan A (two consecutive paragraphs) is simpler but the number appears on a separate line below the equation. Plan B (3-column table) places equation and number on the same visual line. Use Plan B for formal journal submissions or when reviewers specifically require inline numbering. - -For multi-equation papers, keep a running counter and increment `(1)`, `(2)`, etc. manually. - -### D.2 Custom Blocks: Theorem, Definition, Proof - -Two-step pattern required -- borders cannot be set at the style level (see D-4 in Section G). - -```bash -# Step 1: add paragraph with style. Step 2: set border. -officecli add paper.docx /body --type paragraph --prop text="Theorem 1 (Central Limit Theorem). Let X_1, X_2, ..., X_n be i.i.d..." --prop style=Theorem -officecli set paper.docx '/body/p[N]' --prop "pbdr.all=single;4;4472C4;4" - -officecli add paper.docx /body --type paragraph --prop text="Definition 1 (Convergence). A sequence converges..." --prop style=Definition -officecli set paper.docx '/body/p[M]' --prop "pbdr.all=single;4;888888;4" - -# Proof (italic, no border needed) -officecli add paper.docx /body --type paragraph --prop text="Proof. Follows from characteristic function approach. QED." --prop style=Proof -``` - -Multi-paragraph bordered blocks: each paragraph needs its own `pbdr.all` set command. Border format: `style;size;color;space` (e.g., `single;4;4472C4;4` = blue, ~0.5pt, 4pt padding). - -### D.3 Footnotes and Endnotes - -Footnotes are inline reference runs -- they do NOT create new paragraphs and do NOT shift indices. Add in any order. Recommended workflow: add all body content first, then footnotes in forward order. - -> **Note on CLI output:** After adding a footnote, the reference run appears as an **empty string** in `get` and `view annotated` output (`r[N] ""`). This is expected — the run contains a `` XML element, not visible text. To confirm insertion, use `officecli view paper.docx text` and check the footnote count at the bottom, or `officecli get paper.docx '/footnote[N]'` to verify the footnote content. - -```bash -officecli add paper.docx '/body/p[3]' --type footnote --prop text="Smith et al., 2024, Journal of Research, 45(2), pp.112-130." -officecli add paper.docx '/body/p[5]' --type endnote --prop text="See appendix for extended discussion." -``` - -Batch for performance (not required for correctness): - -```bash -officecli add paper.docx /body/p[3] --type footnote --prop text="First footnote." -officecli add paper.docx /body/p[5] --type footnote --prop text="Second footnote." -officecli add paper.docx /body/p[8] --type footnote --prop text="Third footnote." -``` - -### D.4 Tables and Figures - -> **Figure embedding note:** Experimental/empirical papers (lab science, engineering, data-driven research) SHOULD include at least 1 embedded figure — an architecture diagram, results chart, or methodology flowchart. Placeholder captions without embedded images reduce deliverability. If actual image assets are unavailable, generate a representative data chart using `officecli chart` commands and embed it as a figure with caption. Papers that list "Figure 1" in a caption but contain 0 embedded images will be flagged by evaluators. - -For table building blocks (header rows, cell styling, merging), see [docx creating.md](../docx/creating.md#tables----creation--basic-styling). Academic table recipe: - -> **Caption placement rule (APA / academic standard):** -> - **Table captions appear ABOVE the table** — add the caption paragraph BEFORE adding the table. -> - **Figure captions appear BELOW the figure** — add the caption paragraph AFTER adding the image/placeholder. -> -> Placing a table caption below the table violates APA and most academic style guides. This is a common mistake — always add the table caption first. - -```bash -# CORRECT order: caption first, then table -# Step 1: Table caption (ABOVE the table — APA/academic standard) -officecli add paper.docx /body --type paragraph --prop text="Table 1. Participant Demographics" --prop style=Caption --prop spaceAfter=6pt - -# Step 2: Table -officecli add paper.docx /body --type table --prop rows=5 --prop cols=4 --prop style=TableGrid --prop alignment=center --prop width=100% -officecli set paper.docx '/body/tbl[1]/tr[1]' --prop c1="Variable" --prop c2="Mean" --prop c3="SD" --prop c4="N" --prop header=true -# Style each header cell individually (row set does NOT support bold/shd/color) -officecli set paper.docx '/body/tbl[1]/tr[1]/tc[1]' --prop bold=true --prop shd=2E4057 --prop color=FFFFFF -# ... repeat for tc[2], tc[3], tc[4] ... -officecli set paper.docx '/body/tbl[1]/tr[2]' --prop c1="Age" --prop c2="38.4" --prop c3="7.2" --prop c4="47" -officecli set paper.docx '/body/tbl[1]' --prop border.all="single;4;CCCCCC;0" -``` - -**Figure placeholder and Figure legend** (when actual image insertion is not available): - -```bash -# Figure placeholder paragraph (describes where the image goes) -officecli add paper.docx /body --type paragraph \ - --prop text="[Figure 1: CRISPR construct design — insert image here]" \ - --prop italic=true --prop alignment=center --prop size=10 - -# Figure legend paragraph -officecli add paper.docx /body --type paragraph \ - --prop text="Figure 1. CRISPR construct schematic showing guide RNA, Cas9 protein, and target DNA sequence." \ - --prop style=Caption --prop spaceBefore=6 -``` - -Use `italic=true --prop alignment=center` for the placeholder so it is visually distinct from body text. The Figure legend uses the `Caption` style (10pt italic, defined in B.2). - -### D.5 Bibliography - -Each reference is a separate paragraph with hanging indent (0.5" = 720 twips). - -```bash -officecli add paper.docx /body --type paragraph --prop text="References" --prop style=Heading1 -# APA: Author (Year) format, double-spaced -officecli add paper.docx /body --type paragraph --prop text="Barley, S. R., & Kunda, G. (2001). Bringing work back in. Organization Science, 12(1), 76-95." --prop leftIndent=720 --prop hangingIndent=720 --prop font="Times New Roman" --prop size=12 --prop lineSpacing=2x -# Physics: [N] numbered format, 1.5-spaced -officecli add paper.docx /body --type paragraph --prop text="[1] Haldane, F. D. M. (1988). Model for a quantum Hall effect. PRL, 61, 2015." --prop leftIndent=720 --prop hangingIndent=720 --prop font="Times New Roman" --prop size=11 --prop lineSpacing=1.5x -``` - ---- - -## Section E: Polish - -### E.1 Headers and Footers - -> **NOTE:** Adding a field to `footer[1]/p[1]` appends the field to the existing footer paragraph — it does NOT create a new paragraph. A simple centered page number footer works as expected with the two-command sequence below. - -> **WARNING: Do NOT execute footer field commands more than once.** Duplicate execution creates duplicate page numbers (e.g., "6\n6"). After adding a footer, verify with `officecli get paper.docx '/footer[1]'` to confirm only one page field exists. - -```bash -# Simple footer with centered page number -officecli add paper.docx / --type footer --prop alignment=center -officecli add paper.docx '/footer[1]/p[1]' --type field --prop fieldType=page --prop size=10 -``` - -**Running header — recommended for ALL academic paper types:** - -Running headers are expected in academic papers (APA, IEEE, journal submissions, etc.). Add a running header that identifies the document. Typical academic convention: -- **Simple (all pages same):** Shortened paper title, right-aligned, 9pt -- **Mirrored (odd/even pages different):** Odd pages = shortened title; Even pages = author name(s) - -```bash -# Academic running header — shortened title, right-aligned (all pages) -officecli add paper.docx / --type header \ - --prop text="Deep Learning for Gene Expression Prediction" \ - --prop alignment=right --prop size=9 --prop color=444444 - -# Alternative: Author name on all pages (common in APA manuscripts) -officecli add paper.docx / --type header \ - --prop text="Chen & Martinez (2026)" \ - --prop alignment=right --prop size=9 --prop color=444444 -``` - -**Odd/Even page headers (formal journal format):** - -For formal journal submissions that require different headers on odd and even pages (e.g., running title on right pages, author names on left pages): - -```bash -# Step 1: Enable odd/even page header distinction -officecli set paper.docx / --prop differentOddEven=true - -# Step 2: Odd-page header (right-aligned — displays shortened paper title) -officecli add paper.docx / --type header --prop type=default \ - --prop text="Short Title Here" \ - --prop alignment=right --prop size=9 - -# Step 3: Even-page header (left-aligned — displays author name(s)) -officecli add paper.docx / --type header --prop type=even \ - --prop text="Author Names" \ - --prop alignment=left --prop size=9 -``` - -> **Note:** `type=default` sets the odd-page (right-hand) header. `type=even` sets the even-page (left-hand) header. Only use odd/even headers when the journal explicitly requires mirrored pages. - -> **White paper / corporate report header:** -> ```bash -> officecli add paper.docx / --type header --prop text="Organization Name | DOC-ID-001" --prop alignment=right --prop size=9 --prop color=888888 -> ``` - -**Cover page page number — known CLI limitation:** - -Academic convention requires the cover page to NOT display a page number (or use Roman numerals). The current CLI does not support `differentFirstPage` header/footer control. This means the cover page will show the page number "1" in the footer. - -**Known limitation workaround:** After generating the document, inform the user: -> "Note: The cover page displays page number '1' due to a current CLI limitation. To hide it, open the document in Word, go to Insert > Header & Footer, enable 'Different First Page', and delete the content from the first-page footer." - -If you want to document this limitation inline in your delivery notes, use this note pattern. - -**"Page X of Y" pattern:** Currently there is no single-command way to produce "Page X of Y" on one line. The `fieldType=page` and `fieldType=numpages` each create their own paragraph. For a simple page number, use the pattern above. For "Page X of Y", see [docx creating.md](../docx/creating.md#headers--footers) for raw-set workarounds, or accept the limitation and use a simple page number field. - -**Verification after footer setup:** -```bash -officecli get paper.docx '/footer[1]' # Confirm paragraph count and field presence -``` - -> **Note on `get /footer[1]` output:** The footer paragraph contains multiple runs (typically 5: field-char-begin, instrText, field-char-separate, result-text, field-char-end). Most runs appear as empty strings — this is normal. A correctly inserted page field shows `r[4]` with value `"1"` (the cached page number). As long as you see exactly one paragraph and one set of field-char runs, the footer is correct. - -### E.2 Watermark - -```bash -officecli add paper.docx / --type watermark --prop text=CONFIDENTIAL --prop color=DDDDDD -``` - -Watermark, footnotes, and TOC coexist without interference -- verified in v1.0.24. No special ordering required. - -### E.3 Bookmarks and Cross-References - -```bash -# Add bookmark on a heading paragraph -officecli add paper.docx '/body/p[N]' --type bookmark --prop name="thm_chern" --prop text="Theorem 1" - -# Cross-reference from another paragraph (REF field) -officecli add paper.docx '/body/p[M]' --type field --prop instruction=" REF thm_chern \\h " --prop text="Theorem 1" -``` - -> **WARNING: `--prop text="..."` on a bookmark creates a visible text run appended to the paragraph.** If the paragraph already contains the target text (e.g., "Theorem 1"), omit the `text` property to avoid duplicate visible text. Use `--prop text=` only when the paragraph is empty or you want to insert new visible text at the bookmark location. - -REF field text is static until the user updates fields in Word (Ctrl+A, F9). Internal hyperlinks (`#anchor`) are not supported -- use REF field + bookmark instead. For other field types (page numbers, dates), see [docx creating.md](../docx/creating.md#fields). - ---- - -## Section F: QA Checklist - -### F.1 Verification Commands - -```bash -officecli validate paper.docx # XML structure -- must return 0 errors -officecli view paper.docx outline # Heading hierarchy + element counts -officecli view paper.docx issues # Automated issue detection -officecli view paper.docx text # Content verification -officecli view paper.docx annotated # Formatting verification -officecli query paper.docx 'p:empty' # Check for empty spacing paragraphs -``` - -### F.2 Academic-Specific Checks - -- [ ] **H8 (HARD RULE): References/Bibliography section present** — paper includes a final "References" or "Bibliography" section with at minimum 5 formatted citations using hanging indent. A document with inline citations and no reference list is a delivery failure. -- [ ] TOC lists all Heading1/2/3 entries (`view outline` shows heading tree) -- [ ] Every equation renders as OMML (`view text` shows `[Equation]` markers) -- [ ] Footnote markers at correct paragraphs (`view annotated` shows footnote refs) -- [ ] Bibliography has hanging indent on every reference -- [ ] Font hierarchy is consistent: H1 >= 18pt (20pt preferred), H2 >= 14pt, H3 >= 12pt, body = 11-12pt -- [ ] Page margins are 1 inch (1440 twips) on all sides -- [ ] Line spacing matches paper type (double for APA, 1.5x recommended for physics/CS/engineering, 1.5x recommended / 1.15x minimum for white paper — never below 1.15x) -- [ ] No empty paragraphs used as spacing -- [ ] Abstract paragraph has NO `firstLineIndent` (block style — "missing first-line indent" warning from `view issues` is a false positive for Abstract) -- [ ] Multi-column abstract reverts to single-column (`get '/section[N]'` for each section -- all non-abstract sections must show columns=1) -- [ ] Landscape sections revert to portrait - -### F.3 Verification Loop - -1. Generate document -2. Run `validate` + `view outline` + `view issues` + `view text` -3. Fix issues found -4. Re-verify -- one fix often creates another problem -5. Repeat until clean pass. **Do not declare success without at least one fix-and-verify cycle.** - -NOTE: No visual preview for docx (unlike pptx). User must open in Word for visual confirmation. - -**`view issues` 已知误报(可安全忽略)** - -Academic papers routinely trigger 12+ warnings from `view issues`. All of the following are false positives: - -- **Display equations** (`oMathPara` elements): flagged as "empty paragraph" — renders correctly in Word. -- **First-line indent check**: the following paragraph types all trigger "no first-line indent" warnings and are all safe to ignore: - - **Abstract paragraphs** — Abstract uses block style (no first-line indent by design) - - Cover page centered paragraphs (title, authors, affiliations) - - Block quote paragraphs - - Bibliography / references paragraphs with hanging indent (`hangingIndent` set) - - Any paragraph with `leftIndent` or `rightIndent` set (non-zero) -- **Hanging indent false positive**: any paragraph with `leftIndent`/`rightIndent` may also trigger a hanging indent detection warning — ignore. -- **Headings**: headings without first-line indent are expected in all academic styles. - -Do not attempt to "fix" these warnings by adding `firstLineIndent` to cover, block-quote, or reference paragraphs — doing so will break formatting. - ---- - -## Section G: Known Bugs and Lessons - -> **Read these before building. Each one has caused failures in testing.** - -### D-1: Section Break Inserts Empty Paragraph (+1 Index Offset) - -Each `add /body --type section` inserts one empty paragraph. All subsequent `p[N]` indices shift by +1 per section break. - -``` -Before: p[6] = "Methods text" -After section break: p[7] = "" (empty), p[8] = "Results" (shifted +1) -``` - -Always plan section break count and add their offsets to your index calculations. - -### D-2: `\left`/`\right` + Subscript/Superscript Crashes - -``` -WRONG: \left[ x_{i}^{2} \right] --> cast error crash -CORRECT: [ x_{i}^{2} ] --> OMML auto-sizes brackets -``` - -Use plain delimiters `(`, `)`, `[`, `]` in ALL equations. OMML automatically sizes them in display mode. - -### D-3: `\mathcal` Causes Validation Error - -``` -WRONG: \mathcal{L} --> invalid m:scr XML -CORRECT: \mathit{L} --> renders as italic L -``` - -### D-4: Paragraph Borders Cannot Be Set at Style Level - -``` -SILENTLY DROPPED: add /styles --prop "pbdr.all=single;4;4472C4;4" -REJECTED: set /styles/Theorem --prop "pbdr.all=single;4;4472C4;4" -CORRECT: set '/body/p[N]' --prop "pbdr.all=single;4;4472C4;4" -``` - -The raw XML confirms: no `` element is written to the style definition even though the `add` command succeeds without error. - -### D-4b: `pbdr.bottom` XML Element Order Bug (Known CLI Bug — P3) - -When using `--prop pbdr.bottom=...` to add a bottom border to a paragraph (e.g., a cover page divider line), the CLI may generate `` XML with child elements in the wrong order, causing `officecli validate` to report a pBdr-related schema error. - -**Symptom:** `validate` reports an error referencing `w:pBdr` or its child elements after a `set --prop pbdr.bottom=...` command. - -**Workaround:** If validate fails with a pBdr error, replace the `pbdr.bottom` set command with a `raw-set` that writes the complete `` XML directly: - -```bash -# Instead of (may fail due to element order bug): -officecli set paper.docx '/body/p[N]' --prop "pbdr.bottom=single;4;000000;4" - -# Use raw-set with correct element order (requires --xpath and --action): -# Step 1: Remove any existing pBdr (avoids duplicate element) -officecli raw-set paper.docx /document \ - --xpath "//w:body/w:p[N]/w:pPr/w:pBdr" --action remove -# Step 2: Append corrected pBdr to the paragraph's pPr -officecli raw-set paper.docx '/body/p[N]' \ - --xpath './w:pPr' --action append \ - --xml '' -``` - -> **Note:** This is a CLI-level bug (XML serialization order). The skill is not at fault. Mark as P3 — the CLI team is responsible for the fix. Using `raw-set` as shown above is the correct workaround until the bug is resolved. - -### D-5: Shell Escaping for LaTeX Formulas - -- Direct `--prop`: double backslash -- `"formula=\\frac{a}{b}"` -- Batch JSON (no heredoc): quadruple backslash -- `"formula": "\\\\frac{a}{b}"` -- Heredoc batch (recommended): double backslash -- `"formula": "\\frac{a}{b}"` - -For `$` dollar signs in text, see D-10. - -### D-6: Batch JSON Values Must Be Strings - -`CORRECT: {"bold":"true","size":"11"}` / `WRONG: {"bold":true,"size":11}` -- non-string values fail with deserialization error. - -### D-7: Batch Intermittent Failure (~1-in-15) - -May fail with "Failed to send to resident". Keep arrays to 10-15 max, retry on failure, use heredoc syntax. - -### D-8: Table `--index` Positioning Unreliable - -`--index N` on `add /body --type table` may be ignored. Add content in desired order instead. - -### D-9: Internal Hyperlinks Not Supported - -`hyperlink` only accepts `https://...` URIs. Use REF field + bookmark for internal cross-references (Section E.3). - -### D-10: Dollar Sign `$` in Text Content - -Bash interprets `$` inside double quotes as variable expansion. `--prop text="costs $2.8 billion"` silently drops `$2` (undefined variable → empty string). - -``` -WRONG: --prop text="costs $2.8 billion" → "costs .8 billion" -CORRECT: --prop text='costs $2.8 billion' → single quotes prevent expansion -CORRECT: --prop text="costs \$2.8 billion" → escaped dollar sign -``` - -Use single quotes for any text containing `$`. If the text also contains single quotes, use heredoc batch syntax. diff --git a/skills/officecli-data-dashboard/SKILL.md b/skills/officecli-data-dashboard/SKILL.md index 316be6faa..b73d5e0d1 100644 --- a/skills/officecli-data-dashboard/SKILL.md +++ b/skills/officecli-data-dashboard/SKILL.md @@ -1,130 +1,408 @@ --- -# officecli: v1.0.24 name: officecli-data-dashboard -description: "Use this skill when the user wants to create a data dashboard, analytics dashboard, KPI dashboard, or executive summary from CSV/tabular data in Excel format. Trigger on: 'dashboard', 'KPI report', 'analytics summary', 'data visualization', 'CSV to Excel dashboard', 'executive dashboard', 'metrics dashboard'. Output is always a single .xlsx file." +description: "Use this skill to build a multi-element Excel dashboard — Dashboard sheet on open, multiple formula-driven KPI cards, multiple charts, sparklines, and conditional formatting — from CSV or tabular input. Trigger on: 'dashboard', 'KPI dashboard', 'analytics dashboard', 'executive dashboard', 'metrics dashboard', 'CSV to dashboard', 'data visualization'. Output is a single .xlsx. Scene-layer on officecli-xlsx: inherits every xlsx hard rule. DO NOT invoke for: a single budget tracker / one-sheet CSV-with-formatting (use xlsx), a 3-statement / DCF / LBO financial model (use financial-model), a weekly report with ≤ 1 chart and < 10 rows (use xlsx)." --- -# Data Dashboard Skill +# Data Dashboard (scene-layer on officecli-xlsx) -Create professional, formula-driven Excel dashboards from CSV or tabular data. The output is a single `.xlsx` file with a data sheet and a Dashboard sheet -- charts linked to live data, KPIs powered by formulas, and conditional formatting for visual insight. +A dashboard is not "a spreadsheet with charts". It is a composition: **one Dashboard sheet the user lands on** with formula-driven KPI cards, cell-range-linked charts, sparklines, and semantic conditional formatting. Everything else (raw data, aggregations) is upstream infrastructure the user should never need to open. This skill teaches the composition pattern. Everything about the xlsx engine — cells, formulas, batch JSON, shell quoting, validate, HTML preview — comes from `officecli-xlsx` and is not re-taught here. ---- +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. -## BEFORE YOU START (CRITICAL) +## ⚠️ Help-First Rule -**Every time before using officecli, run this check:** +**When a prop name, enum value, or alias is uncertain, consult help before guessing.** ```bash -if ! command -v officecli &> /dev/null; then - echo "Installing officecli..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - # Windows: irm https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.ps1 | iex -else - CURRENT=$(officecli --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - LATEST=$(curl -fsSL https://api.github.com/repos/iOfficeAI/OfficeCLI/releases/latest | grep '"tag_name"' | sed -E 's/.*"v?([0-9.]+)".*/\1/') - if [ "$CURRENT" != "$LATEST" ]; then - echo "Upgrading officecli $CURRENT -> $LATEST..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - else - echo "officecli $CURRENT is up to date" - fi -fi -officecli --version +officecli help xlsx # element list +officecli help xlsx chart # full schema for charts +officecli help xlsx sparkline # sparklines +officecli help xlsx conditionalformatting # all CF rule types ``` ---- +Help reflects the installed CLI version. When this skill and help disagree, **help wins**. DeferredAddKeys (`preset`, `referenceline`, `trendline`, `axisNumFmt`, `holesize`, `combosplit`) work on `add` only — see Reference. -## Use When +## Mental Model & Inheritance -- User wants to create a **dashboard** from CSV data or tabular data -- User asks for **KPI reports**, **analytics summaries**, or **metrics dashboards** -- User wants to **visualize data** in Excel with charts, sparklines, and conditional formatting -- User mentions "CSV to Excel", "executive dashboard", or "data visualization" +This skill **inherits every xlsx hard rule** from `officecli-xlsx` — shell quoting, zero formula errors, visual delivery floor, batch JSON shape (`{"command":"set"|"add","path":...,"props":{...}}` — key is `command`, NOT `action`), batch JSON dotted-name rule, chart data-feed forms, batch+resident limits, `validate` discipline. Read officecli-xlsx first; honour those rules, do not re-teach them here. ---- +**Reverse handoff — do NOT use this skill when:** -## What This Skill Produces +- The ask is a **single-sheet CSV-with-formatting tracker** (no Dashboard sheet, no KPI cards, ≤ 1 chart) → go back to `officecli-xlsx`. +- The ask is a **3-statement / DCF / LBO financial model** with blue-inputs / black-formulas / cross-sheet drivers → use `officecli-financial-model`. +- The ask is a **weekly status report** with one SUMIF summary and one chart over < 10 rows → `officecli-xlsx`. -A single `.xlsx` file with: +This skill only accepts: "a Dashboard sheet the user opens first, multiple KPI cards, multiple charts, some CF / sparklines". -| Component | Sheet | Description | -|-----------|-------|-------------| -| Raw data | Sheet1 | Imported CSV with frozen headers, AutoFilter, column widths, conditional formatting | -| Dashboard | Dashboard | KPI cards (formula-driven), sparklines, charts (cell-range-linked), preset styling | +## Shell & Execution Discipline -The Dashboard sheet is **active on open**. All formulas **recalculate on open**. +→ see officecli-xlsx §Shell & Execution Discipline for the baseline (quoting, heredoc for `!`, incremental execution). ---- +Two increments specific to dashboards: -## Core Concepts +- **Long chart `add` commands exceed 180 chars.** Always split across lines with trailing `\`; never pack a chart command onto a single line. The longer the command, the higher the chance a shell-escape bug hides inside it. +- **Multi-instance counts use `query --json | jq length`, never `raw-get | grep -c`.** Example: `officecli query "$FILE" chart --json | jq '.data.results | length'` for "how many charts do I have?". -### Formula-Driven KPIs -Every KPI value on the Dashboard is a formula referencing the data sheet. Never hardcode calculated values. When the underlying data changes, KPIs update automatically. +## Core Principles -### Cell Range References for Charts -Every chart series references data sheet cells directly (`series1.values="Sheet1!B2:B13"`). Charts stay in sync with data. Never use inline data unless aggregation is impossible in Excel formulas. +Five non-negotiable principles. If any one is violated the output is not a dashboard, it is a spreadsheet that happens to have a chart. -### Chart Presets -Use `preset=dashboard` on charts for datasets with 10+ rows. For datasets with fewer than 10 rows, use `preset=minimal`. See the complexity table in A.3 of creating.md for the authoritative mapping -- **when any other text in this skill conflicts with that table, the table wins.** Presets are DeferredAddKeys -- they work on `add` only, NOT on `set`. A single preset replaces 5-8 manual styling properties with one consistent look. +1. **Formula-driven KPIs.** Every KPI value on the Dashboard sheet is a formula — `SUM`, `AVERAGE`, `IFERROR((...-...)/...,0)`, whatever — referring to cells on the Data / Summary sheet. Never hardcode a computed number. When the underlying data changes tomorrow, KPIs update on open. -### Data-Size-Aware Complexity -The number of KPIs, charts, sparklines, and CF rules scales with the input data size. A 5-row dataset gets 1 chart and no sparklines. A 200-row dataset gets 3-5 KPIs, 2-3 charts, sparklines, and multiple CF rules. +2. **Cell-range references for charts.** Every chart series reads from a cell range: `series1.values="Sheet1!B2:B13"`. Inline `data="Revenue:100,200,300"` is for a 5-minute demo, not a delivered dashboard. The one exception: data requires an aggregation Excel cannot express (rare) — document the exception in a comment cell. ---- +3. **Dashboard-first architecture.** KPI label cells, KPI value cells, charts, sparklines all live on the **Dashboard** sheet — the single sheet a user lands on. Raw imports and `SUMIFS` rollups live on Data / Summary sheets, upstream of the Dashboard. The user should never need to switch tabs to find the answer. -## Workflow Overview +4. **Visible cells only for chart sources.** LibreOffice does not evaluate formulas in hidden columns or hidden sheets at render time. A chart whose `series1.values` points at a hidden-column `SUMIFS` renders blank. Pattern: aggregate into a **visible** Summary sheet, point charts at Summary cells, hide only helper columns that are not chart sources. -### Phase 1: Analyze the Input Data -Count rows and columns. Identify column types (date, numeric, categorical). Determine the primary dimension (X-axis). Look up the data-size-to-complexity table. +5. **Data-size-aware complexity.** A 10-row dataset does not get 5 KPIs and 4 charts. A 200-row dataset does not get 1 KPI and 1 chart. Scale up the composition with the input (table in §Design Ideas). Overbuilding is as wrong as underbuilding. -### Phase 2: Plan Before Building -Decide how many KPIs, which chart types, which CF rules, and chart layout positions. Write out the plan before executing any commands. +## Requirements -### Phase 3: Build the Workbook -Follow the 11-step workflow: create + import, column widths, Dashboard sheet, KPIs, sparklines, charts, conditional formatting, tab colors, polish, raw-set, validate. +All `officecli-xlsx` requirements apply (→ see officecli-xlsx §Requirements for Outputs). Dashboards add these: -### Phase 4: QA -Run the QA checklist. Fix issues. Re-validate. +- **Dashboard sheet is the active tab on open.** Confirm 0-based sheet index with `officecli query "$FILE" sheet` BEFORE filling `activeTab="N"`. Never guess the index. +- **`calc.fullCalcOnLoad=true`.** Set via `officecli set "$FILE" / --prop calc.fullCalcOnLoad=true`. Do NOT `raw-set` `` — it produces duplicate elements that fail validate. +- **Refresh downstream cachedValue after every upstream edit.** `fullCalcOnLoad=true` schedules runtime recalc only; it does NOT refresh build-time `cachedValue`. After `set B=100 → set E==B+D → fix B=150`, E is stale until you re-issue E's formula (or close/reopen). Stale cache ships "Net Change = 0" to the board. +- **Every chart has a descriptive title and every series has a name.** `"Series1"` in a legend is unfinished work. +- **Every KPI value cell has a formula.** Verifiable: `officecli query "$FILE" 'Dashboard!:has(formula)' --json | jq '.data.results | length'` should equal your planned KPI count. +- **Header row fill on every data sheet.** Data sheet, Summary sheet, and any secondary data sheet need row 1 filled (e.g., `fill=1F3864 + font.color=FFFFFF + font.bold=true`). +- **10+ rows on Data sheet → ≥ 1 CF rule on a numeric column.** A 20-row table with zero visual scanning aid is a quality miss. +- **Dashboard value columns sized to the widest expected cachedValue — not a fixed 22.** Rule of thumb at 24pt bold + currency numFmt: `width ≈ ceil((visible_chars + 2) × 1.3)`. A KPI holding `¥1,958,414,250` (14 visible chars with currency + commas) needs `width ≥ 28`; a 4-digit KPI still needs `width ≥ 22` as the floor. Hardcoding `22` for a 10+ digit KPI is how `###` ships to the user. +- **Sparkline row height ≥ 20.** A sparkline in a default 15pt row is a flat squiggle — set `/Dashboard/row[N] height=22` (or 24 when paired with a 24pt KPI value cell in the same row). +- **Print deliverables set `_xlnm.Print_Area` scoped to Dashboard** + hide non-Dashboard sheets + add ``. Without all three, the print pipeline emits every sheet and Dashboard lands on page 2+. See §Print-ready delivery for the exact commands. -### Phase 5: Deliver -Deliver the `.xlsx` file. Tell the user the Dashboard sheet opens first and formulas recalculate automatically. +## Quick Start ---- +Minimal viable dashboard: 12-month revenue CSV → 4 KPIs + 1 line chart + activeTab + fullCalcOnLoad. Adapt the numbers, don't copy-paste blind. Broken into phases so a single failed phase is obvious. -## Full Guide +**Phase 1 — Data sheet: create, import, format.** -Read [creating.md](creating.md) and follow it step by step. It contains the complete workflow, decision tables, command templates, a full runnable example, and the QA checklist. +```bash +FILE=my_dashboard.xlsx +officecli create "$FILE" +officecli import "$FILE" /Sheet1 --file sales.csv --header +officecli set "$FILE" '/Sheet1/col[A]' --prop width=12 +officecli set "$FILE" '/Sheet1/col[B]' --prop width=15 +officecli set "$FILE" '/Sheet1/B2:B13' --prop numFmt='$#,##0' +officecli set "$FILE" '/Sheet1/A1:B1' --prop fill=1F3864 --prop font.color=FFFFFF --prop font.bold=true +``` ---- +**Phase 2 — Dashboard sheet + one KPI card.** -## Quick Reference: Key Warnings - -| Warning | Detail | -|---------|--------| -| Batch invocation | `cat <<'EOF' \| officecli batch dashboard.xlsx` — file argument is **required**. `officecli batch` alone fails with "Required argument missing". | -| Batch JSON values | ALL values must be strings: `"true"` not `true`, `"24"` not `24` | -| Chart preset | Add-only. `preset=dashboard` for 10+ rows, `preset=minimal` for < 10 rows | -| Scatter charts | Use `series1.xValues` NOT `series1.categories` (causes validation error) | -| Reference lines | Format is `value:color:label:dash` (color BEFORE label) | -| Cell range refs | Always `series1.values="Sheet1!B2:B13"`, never inline data | -| raw-set ordering | activeTab raw-set must be the LAST command. Use `set / --prop calc.fullCalcOnLoad=true` for calcPr (not raw-set). | -| formulacf | Do NOT use `font.bold`. Use `fill` + `font.color` only | -| Column widths | `import --header` does NOT auto-size. Set widths manually on **ALL sheets including Dashboard** | -| Dashboard ### | KPI cells at 24pt bold WILL show ### if Dashboard columns are not set to width=22. See Step 4b | -| pie chart | `chartType=pie` has a known rendering issue in LibreOffice — chart renders blank (only legend visible). Use `chartType=doughnut` or `chartType=column` instead for category breakdowns. | -| Dashboard KPI placement | KPI labels and values MUST be on the Dashboard sheet itself, not on Summary/Data sheets. Dashboard must show: KPI area (rows 1-2) + charts (rows 5+). See Step 3 CRITICAL box in creating.md. | -| Helper columns | After using helper columns on Dashboard for chart data sources, hide them with `officecli set file.xlsx '/Dashboard/col[N]' --prop hidden=true`. Visible helper columns are unprofessional clutter. | -| **Chart data source (CRITICAL)** | **Chart series MUST reference visible cells on the Summary sheet or Data sheet — NEVER reference hidden columns or helper columns.** LibreOffice does not recalculate hidden columns on render; charts referencing hidden columns will display blank/empty data. Correct pattern: aggregate all chart data into visible rows on the Summary sheet, then reference those cells as chart data sources. | -| **Charts within print area** | After setting the print area, verify all charts are positioned inside it. Chart `x + width` must not exceed the print area column boundary. Recommended: chart width ≤ 90% of print area width. | -| Summary sheet col widths | Summary sheet columns are NOT auto-sized. Always set widths after writing formulas: text/label cols 15-20, numeric cols 12-15. See Step 2 in creating.md. | -| Percentage numFmt on Summary | `AVERAGEIFS`/ratio formula results on Summary sheet display as raw floats (0.083) unless `numFmt="0.0%"` is set at the same time as the formula. Never skip numFmt on percentage cells. | -| **Header row fill (MANDATORY)** | Every sheet with column headers MUST have a dark fill on row 1: `officecli set dashboard.xlsx '/Sheet1/A1:F1' --prop fill=1F3864 --prop font.color=FFFFFF --prop font.bold=true`. Omitting header fill is a Quality Bar violation (Q2 fail). See Step 2b in creating.md. | -| **Conditional formatting (MANDATORY for 10+ rows)** | At least 1 CF rule (colorscale or databar) MUST be applied to a numeric column in the Data sheet for datasets with 10+ rows. Verify presence with `officecli query dashboard.xlsx 'conditionalformatting'`. Zero CF rules is a Q4 quality failure. | -| **KPI card background fill (Quality Bar)** | Dashboard KPI card area (rows 1-2) SHOULD have a light background fill, e.g., `fill=F0F4FF`. Omitting this is a P2 deduction. See Step 4b in creating.md. | +```bash +officecli add "$FILE" / --type sheet --prop name=Dashboard +officecli set "$FILE" '/Dashboard/col[A]' --prop width=22 +officecli set "$FILE" '/Dashboard/col[B]' --prop width=12 +officecli set "$FILE" /Dashboard/A1 --prop value="Total Revenue" --prop font.size=9 --prop font.color=666666 --prop bold=true +officecli set "$FILE" /Dashboard/A2 --prop 'formula==SUM(Sheet1!B2:B13)' --prop numFmt='$#,##0' --prop font.size=24 --prop bold=true --prop font.color=2E7D32 +``` ---- +**Phase 3 — Sparkline + chart.** + +```bash +officecli add "$FILE" /Dashboard --type sparkline --prop cell=B2 --prop range='Sheet1!B2:B13' --prop type=line --prop color=4472C4 --prop highPoint=true --prop highMarkerColor=FF0000 +officecli add "$FILE" /Dashboard --type chart \ + --prop chartType=line \ + --prop title="Revenue Trend" \ + --prop series1.name="Revenue" \ + --prop series1.values='Sheet1!B2:B13' \ + --prop series1.categories='Sheet1!A2:A13' \ + --prop preset=dashboard --prop axisNumFmt='$#,##0' \ + --prop x=0 --prop y=5 --prop width=10 --prop height=15 +``` + +**Phase 4 — fullCalcOnLoad → activeTab (LAST) → close → validate.** + +```bash +officecli set "$FILE" / --prop calc.fullCalcOnLoad=true + +# Resolve Dashboard's 0-based index from the actual sheet list — never hardcode. +DASH_IDX=$(officecli query "$FILE" sheet --json \ + | jq '[.data.results[].path] | index("/Dashboard")') +officecli raw-set "$FILE" /workbook --xpath "//x:sheets" --action insertbefore \ + --xml "" +officecli close "$FILE" +officecli validate "$FILE" +``` + +Verified end-to-end on a 12-row revenue CSV: `validate` reports no errors, Dashboard opens first, `Dashboard/A2.cachedValue` resolves (2,075,000 for the test data), chart renders with values linked. + +## Design Ideas + +Options, not templates. The user's data and audience drive the choices. + +### Layout patterns (pick one, stay consistent) + +**Pattern 1 — executive summary** (board packs): KPI strip A1:H4, charts stack from row 6. +``` +┌ KPI1 │ KPI2 │ KPI3 │ KPI4 ┐ rows 1-4 +├──────┴──────┴──────┴──────┤ +│ Chart 1 (wide) │ rows 6-18 +├───────────────┬───────────┤ +│ Chart 2 │ Chart 3 │ rows 20-32 +``` + +**Pattern 2 — ops console** (live ops): KPIs down A:B, charts fill C:L. +``` +│ KPI1 │ │ +│ KPI2 │ Chart 1 │ rows 1-12 +│ KPI3 │ │ +│ KPI4 ├───────────────────┤ +│ KPI5 │ Chart 2 │ rows 14-26 +``` + +**Pattern 3 — scorecard** (≥ 6 KPIs, no dominant chart): grid of 2×3 cards (label / value / sparkline). +``` +│ KPI1 │ KPI2 │ KPI3 │ rows 1-4 +│ KPI4 │ KPI5 │ KPI6 │ rows 5-8 +``` + +### Complexity scaling by data size + +| Rows | KPIs | Charts | Sparklines | CF rules | Preset | +|---|---|---|---|---|---| +| < 10 | 1–2 | 1 | skip | 0–1 | `minimal` | +| 10–50 | 2–3 | 2 | only if sequential time-series | 1–2 | `dashboard` | +| 50–200 | 3–5 | 2–3 | only if sequential time-series | 2–3 | `dashboard` | +| 200+ | 3–5 | 3 | only if sequential time-series | 3–4 | `dashboard` | + +### Chart type selection + +| Data pattern | Chart type | Notes | +|---|---|---| +| Trend over time, one series | `line` | Add `trendline=linear` to show direction on noisy series | +| Trend over time, multiple components | `line` (multi-series) or `columnStacked` | Stacked when components sum to a meaningful total | +| Comparison across categories in time order | `column` | Not `bar` — horizontal bars break left-to-right time reading | +| Part-of-whole breakdown | `doughnut` | Prefer over `pie`: `chartType=pie` has a known LibreOffice blank-render regression | +| Budget vs actual | `combo` with `combosplit=1` | First series as bars, rest as lines | +| Correlation | `scatter` | Uses `series1.xValues`, NOT `series1.categories` | + +### Preset options + +`--prop preset=` on every chart. Options: `minimal`, `dashboard`, `corporate`, `magazine`, `colorful`, `monochrome`, `dark`. Pick one and stay consistent across all charts on a single Dashboard — mixing presets reads as accidental. + +### Conditional formatting — semantic colors + +Four CF rule types; each uses `--type ` at `add` time: + +| Intent | `--type` | Typical props | +|---|---|---| +| Magnitude bar (sales, spend) | `databar` | `sqref=B2:B13 color=4472C4 min=0 max=` — always set explicit `min`/`max`; defaults emit invalid XML | +| Heat map (rates, growth) | `colorscale` | `sqref=D2:D13 mincolor=FFCDD2 midcolor=FFFFFF maxcolor=C8E6C9` | +| Status indicator | `iconset` | `sqref=E2:E13 iconset=3Arrows` — see help for the full enum | +| Custom business rule | `formulacf` | `sqref=B2:B13 'formula=$B2>=100000' fill=C8E6C9 font.color=2E7D32` — NEVER `font.bold` (schema rejects ``) | + +Semantic colors to stay consistent within a dashboard: + +- good / positive: fill `C8E6C9`, font `2E7D32` +- bad / negative: fill `FFCDD2`, font `C62828` +- neutral: fill `F5F5F5`, font `666666` + +### KPI card anatomy + +A card is a label cell + a value cell. The label is small gray (font.size=9, font.color=666666, bold); the value is large bold (font.size=24, bold=true, numFmt, font.color signals tone). One row of light fill (e.g. `F0F4FF`) across the card area gives the "card" read without building merged-cell scaffolds. Value column width must be sized to the largest cachedValue — never narrower than 22, often 26–32 for 8+ digit currency (see Requirements). + +### Chart width budget by title length + +At the `dashboard` preset's default title font, the chart plot-box width (in column units) must stay ahead of the title string, or the title clips mid-word. Rule of thumb: `chart.width ≥ ceil(title.length × 0.18)`. A 35-character title ("Department: Year-End Headcount vs Attrition Rate") needs `width ≥ 7`; be safer and use 10–12. If the anchor cannot be widened, shorten the title to ≤ 25 characters — clipped titles in a board-ready deliverable are indefensible. + +`officecli get chart[N]` does not expose numeric `width` on 1.0.63 — it returns `.data.format.anchor` (e.g. `"A6:K21"`). Derive column span from letters (A→K = 10 cols) for Gate 2. + +### Print-ready delivery (board-pack / investor-send / one-pager) + +Triggers: ask contains "print" / "一页" / "董事会" / "投资人". Four artefacts on the Dashboard sheet; non-Dashboard sheets hidden so the print pipeline emits one page only. + +```bash +# 1. Print_Area scoped to Dashboard (xlnm convention). +officecli add "$FILE" / --type namedrange --prop name=_xlnm.Print_Area --prop scope=Dashboard --prop 'refersTo=Dashboard!$A$1:$H$36' +# 2. fit-to-page on Dashboard. +officecli raw-set "$FILE" /Dashboard --xpath "//x:worksheet" --action prepend --xml '' +# 3. Landscape page setup. +officecli raw-set "$FILE" /Dashboard --xpath "//x:sheetData" --action insertafter --xml '' +# 4. Hide non-Dashboard sheets — Print_Area scope alone does NOT stop the print pipeline from emitting every visible sheet. +for S in Sheet1 Summary; do + officecli raw-set "$FILE" /workbook --xpath "//x:sheet[@name='$S']" --action setattr --xml "state=hidden" || true +done +``` + +Delete any `Print_Area` set on Data / Summary sheets — conflicting scopes emit multi-page output. + +## QA (REQUIRED — Delivery Gate) + +**Assume there are problems. Your job is to find them.** A chart that was rendered does not mean a chart that was meaningful. "validate pass" is not delivery; "the Dashboard sheet reads like someone who knows the business made it" is delivery. + +### Minimum cycle before "done" + +Inherit the xlsx baseline (`view issues`, formula error queries, `validate`, HTML preview scan): → see officecli-xlsx §QA minimum cycle. + +Then run the dashboard-specific Delivery Gates. Each gate uses **COUNT-then-if** pattern with a `.data.*` wrapper — never chain `&& echo OK || echo FAIL`. + +**Gate 1 — KPI formula coverage.** Every planned KPI cell must carry a formula. Adjust `-lt 2` to your plan (4 KPIs → `-lt 4`). + +```bash +KPI_FORMULAS=$(officecli query "$FILE" 'Dashboard!:has(formula)' --json | jq '.data.results | length') +[ "$KPI_FORMULAS" -lt 2 ] && { echo "REJECT Gate 1: $KPI_FORMULAS formula cells on Dashboard"; exit 1; } +``` + +**Gate 2 — Chart count matches plan, every chart has data + plausible title width.** + +```bash +CHART_COUNT=$(officecli query "$FILE" chart --json | jq '.data.results | length') +[ "$CHART_COUNT" -lt 1 ] && { echo "REJECT Gate 2: zero charts"; exit 1; } +col_num () { local c=$1 n=0; for ((k=0;k<${#c};k++)); do n=$((n*26+$(printf '%d' "'${c:$k:1}")-64)); done; echo "$n"; } +for i in $(seq 1 "$CHART_COUNT"); do + JSON=$(officecli get "$FILE" "/Dashboard/chart[$i]" --json) + SC=$(echo "$JSON" | jq -r '.data.format.seriesCount // 0') + TITLE=$(echo "$JSON" | jq -r '.data.format.title // ""') + ANCHOR=$(echo "$JSON" | jq -r '.data.format.anchor // ""') + [ "$SC" = "0" ] || [ -z "$TITLE" ] && { echo "REJECT Gate 2: chart[$i] seriesCount=$SC title='$TITLE'"; exit 1; } + [ -z "$ANCHOR" ] && continue + LCOL=$(echo "${ANCHOR%%:*}" | sed 's/[0-9]*$//'); RCOL=$(echo "${ANCHOR##*:}" | sed 's/[0-9]*$//') + SPAN=$(( $(col_num "$RCOL") - $(col_num "$LCOL") + 1 )) + MIN=$(( (${#TITLE} * 18 + 99) / 100 )) + [ "$SPAN" -lt "$MIN" ] && { echo "REJECT Gate 2: chart[$i] title=${#TITLE} chars needs width ≥ $MIN, anchor spans $SPAN"; exit 1; } +done +``` + +Narrower titles at preset `minimal` / `magazine` may clip earlier than the 0.18 factor — spot-check. + +**Gate 3 — Chart series names populated (no "Series1" in legend).** + +```bash +for i in $(seq 1 "$CHART_COUNT"); do + BAD=$(officecli get "$FILE" "/Dashboard/chart[$i]" --json | jq '[.data.children[]? | select(.type == "series") | select((.format.name // "") | test("^Series[0-9]+$"; "i"))] | length') + [ "$BAD" -gt 0 ] && { echo "REJECT Gate 3: chart[$i] has $BAD auto-named series"; exit 1; } +done +``` + +**Gate 4 — CF rules on Data sheet (10+ rows).** + +```bash +CF_COUNT=$(officecli query "$FILE" conditionalformatting --json | jq '.data.results | length') +[ "$CF_COUNT" -lt 1 ] && { echo "REJECT Gate 4: zero CF rules on 10+ row data sheet"; exit 1; } +``` + +Note: `query conditionalformatting` is the canonical element name; `query cf` returns 0 (not an alias). + +**Gate 5 — activeTab and fullCalcOnLoad set.** Compare against real Dashboard index (Dashboard-at-index-0 is a true pass). + +```bash +DASH_IDX=$(officecli query "$FILE" sheet --json | jq '[.data.results[].path] | index("/Dashboard")') +ACTIVE=$(officecli get "$FILE" /workbook --json | jq '.data.format.activeTab // -1') +FULLCALC=$(officecli get "$FILE" /workbook --json | jq -r '.data.format["calc.fullCalcOnLoad"] // false') +[ "$ACTIVE" != "$DASH_IDX" ] && { echo "REJECT Gate 5: activeTab=$ACTIVE Dashboard=$DASH_IDX"; exit 1; } +[ "$FULLCALC" != "true" ] && { echo "REJECT Gate 5: calc.fullCalcOnLoad=$FULLCALC — stale caches will ship"; exit 1; } +``` + +**Gate 6 — Placeholder sweep.** No build-time tokens in rendered output. + +```bash +LEAKS=$(officecli view "$FILE" text 2>/dev/null | grep -niE '\{\{|\$fy\$||xxxx|TBD' | wc -l | tr -d ' ') +[ "$LEAKS" -gt 0 ] && { echo "REJECT Gate 6: $LEAKS placeholder tokens"; exit 1; } +``` + +**Gate 7 — Visual delivery floor (ported from xlsx).** Run `officecli view "$FILE" html` and Read the returned HTML path. Confirm: + +- No `###` in any Dashboard or Data cell (columns too narrow). +- No truncated KPI labels, sheet tab names, or chart titles. +- No placeholder tokens rendered as text (`$fy$24`, `{var}`, ``, `xxxx`). +- Pie / doughnut slices render with distinct fill colors (if collapsed in LibreOffice, verify in the user's target viewer before declaring broken — → see officecli-xlsx §Known Issues/Renderer caveats). +- No empty chart anchors — every chart has a visible, plausible plot. +- Dashboard sheet opens first (tab highlighted, active area scrolled to top). + +If `view html` is blocked (renderer conflict, headless, port busy), Gate 7 is still **mandatory** — run ALL fallback checks: + +```bash +# a) Token / ### sweep. +officecli view "$FILE" text 2>/dev/null | grep -nE '###|\{\{||\$fy\$|xxxx' && { echo "REJECT Gate 7: tokens or ### present"; exit 1; } +# b) Per-KPI: cachedValue length × coef must fit col width. coef=0.55 fit-to-page, 0.85 otherwise. +for CELL in A2 C2 E2 G2; do + CV=$(officecli get "$FILE" "/Dashboard/$CELL" --json | jq -r '.data.format.cachedValue // .data.text // ""') + W=$(officecli get "$FILE" "/Dashboard/col[${CELL%%[0-9]*}]" --json | jq -r '.data.format.width // 0') + CAP=$(echo "$W * 0.55" | bc -l | awk '{print int($1)}') + [ "${#CV}" -gt "$CAP" ] && { echo "REJECT Gate 7: $CELL '$CV' (${#CV} chars) > cap $CAP"; exit 1; } +done +# c) Rerun Gate 2 title × 0.18 ≤ anchor span. d) Log which fallback was used and why. +``` + +Gate 7 must **NEVER** be skipped — skipping ships `###` to the user. + +If scene keywords include print / 一页 / board / 投资人 / 董事会, extend Gate 7 with a structural print-scope check: + +```bash +if echo "$USER_REQ" | grep -qiE 'print|一页|投资人|董事会|board'; then + # Every non-Dashboard sheet must be hidden or veryHidden. + LEAKING=$(officecli query "$FILE" 'sheet' --json | jq -r '.data.results[] | select(.name != "Dashboard" and (.state // "visible") == "visible") | .name') + [ -n "$LEAKING" ] && { echo "REJECT Gate 7 print-scope: visible non-Dashboard sheet(s): $LEAKING — hide before delivery"; exit 1; } + # Dashboard must carry an explicit Print_Area named range. + PA=$(officecli query "$FILE" 'namedrange[name="_xlnm.Print_Area"]' --json | jq '.data.results | length') + [ "$PA" -ge 1 ] || { echo "REJECT Gate 7 print-scope: no _xlnm.Print_Area set"; exit 1; } +fi +``` + +The user opens the file in their target viewer (Office / WPS / Numbers) for the final print preview — the skill does not render export artefacts. + +**Gate 8 — Formula sanity (cachedValue real, not stale/error).** `fullCalcOnLoad=true` refreshes at runtime, NOT build-time cache — so every formula cell must carry a non-empty, non-zero, non-error `cachedValue` now. + +```bash +for CELL in A2 C2 E2 G2; do + JSON=$(officecli get "$FILE" "/Dashboard/$CELL" --json) + [ -z "$(echo "$JSON" | jq -r '.data.format.formula // ""')" ] && continue + CV=$(echo "$JSON" | jq -r '.data.format.cachedValue // ""') + case "$CV" in + "" | "0" | "#DIV/0!" | "#REF!" | "#N/A" | "#VALUE!" | "#NAME?" | "null") + echo "REJECT Gate 8: $CELL cachedValue='$CV' — re-issue formula or close+reopen"; exit 1 ;; + esac +done +``` + +If a KPI is genuinely zero (e.g. "terminations this quarter" = 0), whitelist it in the loop and document — default assumption is "zero is broken". + +If anything fails, fix at source, re-run the full cycle. + +### Honest limits + +Scatter's `series1.xValues` is not exposed in `get --json` (series `values=""`) — use chart-level `seriesCount`. LibreOffice chart color drift / pie-slice collapse / checkbox double-box are viewer artifacts — spot-check in Office / WPS / Numbers first. + +## Reference + +- **Shorthand `--type` at `add`:** `chart`, `sparkline`, `databar`, `colorscale`, `iconset`, `formulacf`. CF rules map to `help xlsx conditionalformatting`; path suffix `/Sheet/cf[N]`. +- **Full schemas live in help:** `officecli help xlsx chart` / `sparkline` / `conditionalformatting`. This skill does not mirror them. +- **DeferredAddKeys (add-only, ignored on `set`):** `preset`, `trendline`, `referenceline`, `axisNumFmt`, `combosplit`, `holesize`. See D-1. +- **Build order:** charts + sparklines + CF + tabColors first → `calc.fullCalcOnLoad=true` via high-level `set` → `raw-set activeTab` **LAST** (after all sheets exist). + +## Known Issues & Pitfalls + +### Dashboard-specific + +| # | Issue | Mitigation | +|---|---|---| +| D-1 | `preset`, `referenceline`, `trendline`, `axisNumFmt` are DeferredAddKeys — work on `add` only, silently ignored on `set` | Include them at `add` time. Cannot apply after the fact — remove + re-add. | +| D-2 | `referenceline` format is `value:color:label:dash` (color BEFORE label). `"0:Break-Even:FF0000:dash"` fails `Invalid color value`. | Order is value, color, label, dash. | +| D-3 | Scatter charts use `series1.xValues`, not `series1.categories`. `` inside `` is schema-invalid. | `--prop series1.xValues="Sheet1!A2:A13"` | +| D-4 | `formulacf` rejects `font.bold` (dxf/font schema disallows ``). | Use `fill` + `font.color` only; bold is not available via CF. | +| D-5 | Dashboard column widths default to 8.43 — KPI values at 24pt bold show `###` | Size by cachedValue bracket: 4–6 digits → 22–24; 7–9 digits (million) → 26–30; 10+ digits (亿 / billion) → 32–36; 百亿 / 10-digit + currency symbol + fit-to-page landscape → **40–44**. Formula `ceil((visible_chars+2)*1.3)` is a starting point; always verify via Gate 7 fallback b). Sparkline columns: 12. | +| D-6 | `raw-set activeTab` must be the LAST mutation. Inserting before all sheets exist shifts indices. | Finish all sheets / charts / CF / sparklines / tabColors, then `raw-set`. | +| D-7 | `calc.fullCalcOnLoad` via `raw-set` creates duplicate `` → validate fails | Use `officecli set "$FILE" / --prop calc.fullCalcOnLoad=true`. | +| D-8 | LibreOffice does not evaluate hidden-column formulas at render → charts referencing hidden cells render blank | Aggregate into a visible Summary sheet, chart reads from Summary. Hide only columns that are not chart sources. | +| D-9 | `chartType=pie` blank-renders in LibreOffice (v1.0.x) | Use `doughnut` as the safe substitute for part-of-whole breakdowns. | +| D-10 | `SUMIFS` / `AVERAGEIFS` with date criteria fails silently if the criterion is a string | Wrap with `DATE()` or `DATEVALUE()`: `=SUMIFS(B2:B13,A2:A13,DATE(2025,1,5))`. | +| D-11 | Summary sheet percentage formulas display as raw decimals (0.098) without `numFmt` | Set `numFmt="0.0%"` at the same `set` call as the formula. | +| D-12 | `import --header` sets freeze + AutoFilter but does NOT set column widths; `numFmt` on a `col[]` path is rejected | Set widths on `col[]`; set `numFmt` on the cell range (`A2:A13`), not the column. | +| D-13 | Sparkline `highpoint` is a bool (highlight on/off), not a color. `--prop highpoint=FF0000` errors `Invalid boolean value` | `--prop highPoint=true --prop highMarkerColor=FF0000`. Same pattern for lowPoint / firstPoint / lastPoint and their *MarkerColor. | +| D-14 | Sparkline cross-sectional data is meaningless (a region or department has no ordering) | Skip sparklines unless rows are a sequential time-series (dates, months, quarters). | +| D-15 | 1.0.63+ rejects empty chart `add` (`Chart requires data`) at the CLI layer — legacy skills that relied on silent accept will fail here | Always provide `series1.values=` / `dataRange=` / inline `data=` at chart `add` time. Treat Gate 2 seriesCount check as a belt-and-braces verification. | +| D-16 | `fullCalcOnLoad=true` guarantees a **runtime** recalc when the end user opens the file; it does NOT refresh the build-time `cachedValue` in XML. Build sequence `set B=100 → set E==B+D → fix B=150` leaves `E.cachedValue` stale (board sees "Net Change = 0"). | After all upstream edits are final, re-issue every downstream formula (`officecli set "$FILE" /Sheet/E2 --prop formula==B2+D2`) OR `close` + re-open the file. Gate 8 verifies. | +| D-17 | 1.0.63 built-in calc engine does NOT evaluate `SUMPRODUCT` with array-predicate form `SUMPRODUCT((A2:A97=X)*C2:C97*D2:D97)` — cachedValue stays `0`/`null`, Gate 8 rejects. Runtime Excel / WPS compute fine, but board-delivered XLSX with stale cache still ships `0`. | Rewrite as helper column + `SUMIF`: `F2==C2*D2` on source sheet, then `=SUMIF(B:B, "Region X", F:F)`. Or pre-aggregate in Summary sheet and chart from there. | -## References +### Inherited (pointer only) -- [creating.md](creating.md) -- Complete dashboard creation guide (the main skill file) -- [xlsx SKILL.md](../xlsx/SKILL.md) -- General xlsx reading, editing, and QA reference +Cross-sheet `!` trap, batch + resident for formulas, `labelRotation` on axis-by-role, `chartType=pareto`, `validate` while resident, data bar without explicit `min`/`max`, chart `anchor` / series immutability after create → see officecli-xlsx §Known Issues. diff --git a/skills/officecli-data-dashboard/creating.md b/skills/officecli-data-dashboard/creating.md deleted file mode 100644 index 1af3437f6..000000000 --- a/skills/officecli-data-dashboard/creating.md +++ /dev/null @@ -1,1238 +0,0 @@ - - -# Creating a Data Dashboard - -Complete guide for building a professional Excel dashboard from CSV or tabular data. Follow this step by step. - ---- - -## Section A: Overview and Decision Logic - -### A.1 What You Will Build - -A single `.xlsx` file with two sheets: - -1. **Sheet1 (Data)** -- Imported CSV data with frozen headers, AutoFilter, column widths, and conditional formatting on key columns. -2. **Dashboard** -- KPI cards (formula-driven), sparklines, charts (linked to data via cell range references), and preset styling. - -Three non-negotiable principles: - -- **All KPI values are formulas**, never hardcoded numbers -- **All charts use cell range references**, never inline data -- **Dashboard is the active sheet on open**, formulas recalculate on open - -### A.2 Analyze the Input Data (MANDATORY FIRST STEP) - -Before writing any commands, examine the CSV: - -1. **Count rows and columns** in the CSV -2. **Identify column types:** - - Date/time (the usual X-axis / primary dimension) - - Numeric: currency, count, rate, percentage - - Categorical: text labels (region, product, department) -3. **Determine the primary dimension** -- what the X-axis will be (usually date or category) -4. **Note the row count** -- this determines output complexity (see A.3) - -### A.2b Handling Multi-Dimensional Data (MANDATORY CHECK) - -> **DETECTION:** After Step A.2, check if any categorical column has repeating values (e.g., "North" appears in multiple rows, or "Q1" appears for every department). If YES, your data is multi-dimensional -- you MUST follow this section. Skipping this leads to noisy, unreadable charts. - -If the data has **multiple categorical dimensions** (e.g., region x category x date, or department x quarter), it is NOT a simple time series. Follow these guidelines: - -1. **Identify the aggregation strategy.** Decide which dimension(s) are most important for the dashboard's purpose. Common patterns: - - **Time + Category**: Aggregate across categories to show time trends (e.g., `SUMIFS` by date). - - **Category + Subcategory**: Show totals per top-level category (e.g., revenue by region). - - **Cross-tabulation**: If the user wants breakdowns by two dimensions, pick the primary one for charts and use KPIs for the secondary. - -2. **KPI formulas for multi-dimensional data.** Use aggregation functions that work across the full dataset: - - `=SUM(col)` for totals (works regardless of dimensions) - - `=AVERAGEIFS(value_col, criteria_col, criteria)` for conditional averages - - `=COUNTIF(col, criteria)` for counting categories - - `=SUMIFS(value_col, criteria_col, criteria)` for conditional sums - - > **CRITICAL — numFmt for percentage results (Fix-2):** When writing `AVERAGEIFS` or other formulas to Summary sheet cells where the result is a percentage (e.g., conversion rate, churn rate), you MUST set `numFmt` at the same time. Without it, the cell displays as a raw float (e.g., `0.0983333`) instead of a readable percentage. - > - > ```bash - > # When setting a percentage formula on Summary sheet, always include numFmt: - > officecli set dashboard.xlsx "Summary!C2" --prop "formula==AVERAGEIFS(Data!C2:C100,Data!A2:A100,B2)" --prop numFmt="0.0%" - > # Or in batch JSON: - > # {"command":"set","path":"Summary!C2","props":{"formula":"=AVERAGEIFS(Data!C2:C100,Data!A2:A100,B2)","numFmt":"0.0%"}} - > ``` - > - > Common numFmt values for Summary sheet formula results: - > | Result Type | numFmt | - > |-------------|--------| - > | Percentage (e.g., rate, ratio) | `0.0%` or `0%` | - > | Currency | `$#,##0` or `#,##0` | - > | Integer count | `#,##0` | - > | Decimal | `#,##0.00` | - -3. **Charts with multi-dimensional data.** When rows are not 1:1 with the X-axis (e.g., 20 rows per month across 4 regions x 5 categories): - - Charts using raw cell ranges will show all rows (e.g., 200 data points). This is acceptable for scatter and line charts but may be noisy. - - For aggregated views (e.g., "Revenue by Region"), create a **helper summary area** on the Dashboard sheet using `SUMIFS`/`AVERAGEIFS` formulas, then reference those cells as the chart data source. - - Document any helper cells explicitly so the dashboard remains maintainable. - -4. **Sparklines with multi-dimensional data.** Sparklines show 1D trends. If the data is cross-sectional (not a meaningful time series when read row-by-row), sparklines may not be useful. In that case, it is acceptable to skip sparklines even if the complexity table says "YES" or "Optional" -- add a comment explaining why. - -5. **Hiding helper columns (MANDATORY).** If you create a helper summary area on the Dashboard sheet for chart data sources, the helper columns MUST be hidden before delivery. Exposed helper columns (raw numbers in columns I, J, K etc.) appear as clutter alongside the KPI cards and charts. - - > **CRITICAL — Chart data source MUST reference visible Summary sheet cells, NOT hidden columns.** - > LibreOffice does not recalculate hidden column formulas at render time. If a chart's `series.values` points to a hidden helper column (even one with valid `SUMIFS`/`AVERAGEIFS` formulas), the chart will render as empty/blank because the formula results are never computed. - > - > **Correct pattern:** - > 1. Write all aggregation formulas (`SUMIFS`, `AVERAGEIFS`) to a **visible** Summary sheet (a dedicated sheet, not hidden columns on Dashboard). - > 2. Set chart `series.values` / `series.categories` to reference the **Summary sheet cells**. - > 3. After charts are created and verified, you may hide the Summary sheet if desired — but verify charts still render before hiding. - > - > **Wrong pattern (causes blank charts):** - > - Writing `SUMIFS` formulas to hidden Dashboard columns (e.g., `col[I]` → hidden) - > - Pointing chart `series.values` to those hidden cells - > - Chart data appears blank in LibreOffice/WPS because hidden column formulas are not evaluated - - **Option A — Use a dedicated Summary sheet (recommended to avoid blank charts):** - ```bash - officecli add dashboard.xlsx / --type sheet --prop name=Summary - # Write aggregation formulas to Summary sheet (visible cells) - # Point chart series.values to Summary sheet cells - # Optionally hide Summary sheet after chart verification: - officecli set dashboard.xlsx /Summary --prop hidden=true - ``` - - **Option B — Hide helper columns on Dashboard (ONLY if charts do NOT reference the hidden columns):** - ```bash - # Only hide columns that are NOT used as chart data sources - officecli set dashboard.xlsx '/Dashboard/col[I]' --prop hidden=true - officecli set dashboard.xlsx '/Dashboard/col[J]' --prop hidden=true - # Repeat for each helper column used - ``` - - After hiding, verify with `officecli view dashboard.xlsx text` that only KPI cards and chart titles are visible on Dashboard. - -### A.3 Data Size to Complexity Mapping - -> **This table is the authoritative source for output complexity.** When any other text in this skill (including Core Concepts in SKILL.md) conflicts with this table, **this table wins.** - -| Data Size | KPIs | Charts | Sparklines | CF Rules | Named Ranges | Preset | -|-----------|------|--------|------------|----------|--------------|--------| -| < 10 rows | 1-2 | 1 | NO | 0-1 | NO | minimal | -| 10-50 rows | 2-3 | 2 | Optional (time-series only) | 1-2 | NO | dashboard | -| 50-200 rows | 3-5 | 2-3 | YES (time-series only) | 2-3 | Optional | dashboard | -| 200+ rows | 3-5 | 3 | YES (time-series only) | 3-4 | Recommended | dashboard | - -Sparkline column: "YES/Optional" applies only when data is a **sequential time series**. For cross-sectional or categorical data, skip sparklines regardless of row count (see Step 5 decision gate). - -> **STOP here and plan.** Before writing any commands, write out: -> 1. How many KPIs? Which formulas? -> 2. How many charts? Which types? Which data columns? -> 3. Which CF rules? On which columns? -> 4. Chart layout positions (use the grid in A.5) - -### A.4 Chart Type Selection Guide - -| Data Pattern | Recommended Chart | Example | -|-------------|-------------------|---------| -| Trend over time (single series) | `line` | MRR over 12 months | -| Trend over time (multiple series) | `line` (multi-series) or `columnStacked` | Revenue components over time | -| Comparison across categories | `column` or `bar` | Revenue by region — for time-series data (dates, months, quarters in order), prefer `column` over `bar`; `bar` creates horizontal bars which make left-to-right time reading unnatural | -| Part-of-whole breakdown | `doughnut` or `pie` | Spend by category | -| Budget vs Actual | `combo` (bars + line) | Department budget performance | -| Correlation | `scatter` (see note below) | Price vs volume | - -> **Scatter chart syntax differs from other charts.** Scatter charts do NOT use `series1.categories`. Instead, use `series1.xValues` for the X-axis data. Using `series1.categories` on a scatter chart produces an invalid `` element in the OOXML and will fail validation. See the scatter chart template in Step 6. - -### A.5 Dashboard Layout Grid - -``` -Dashboard Sheet Layout: -+----------------------------------------------------------+ -| Row 1-4: KPI Cards | -| A1: Label (9pt gray) C1: Label E1: Label G1: Label | -| A2: Value (24pt bold) C2: Value E2: Value G2: Value | -| B2: Sparkline D2: Spark F2: Spark H2: Spark | -+----------------------------------------------------------+ -| Row 5+: Charts (2-column grid) | -| Left: x=0, y=5, width=10, height=15 | -| Right: x=11, y=5, width=10, height=15 | -+----------------------------------------------------------+ -| Row 21+: Additional charts (if needed) | -| Left: x=0, y=21, width=10, height=15 | -| Right: x=11, y=21, width=10, height=15 | -+----------------------------------------------------------+ -``` - ---- - -## Section B: Step-by-Step Workflow - -### Step 1: Create Workbook and Import Data - -**Option A — Import from CSV (most common):** - -```bash -officecli create dashboard.xlsx -officecli import dashboard.xlsx /Sheet1 --file data.csv --header -``` - -`--header` automatically sets freeze pane at A2 and AutoFilter. After import, note the **last data row number** (e.g., row 13 for 12 data rows + 1 header). - -**Option B — Build data sheet from scratch (no CSV):** - -```bash -officecli create dashboard.xlsx -officecli add dashboard.xlsx / --type sheet --prop name=Data -officecli remove dashboard.xlsx /Sheet1 - -officecli open dashboard.xlsx - -# Write headers in row 1 -cat <<'EOF' | officecli batch dashboard.xlsx -[ - {"command":"set","path":"/Data/A1","props":{"value":"Month","bold":"true"}}, - {"command":"set","path":"/Data/B1","props":{"value":"Revenue","bold":"true"}}, - {"command":"set","path":"/Data/C1","props":{"value":"Units","bold":"true"}}, - {"command":"set","path":"/Data/D1","props":{"value":"Target","bold":"true"}} -] -EOF - -# Write data rows via batch — all values must be strings -cat <<'EOF' | officecli batch dashboard.xlsx -[ - {"command":"set","path":"/Data/A2","props":{"value":"Jan"}}, - {"command":"set","path":"/Data/B2","props":{"value":"120000"}}, - ... -] -EOF - -# Set freeze pane manually (equivalent to import --header) -officecli set dashboard.xlsx /Data --prop freeze=A2 -# Set AutoFilter — must target the sheet path, NOT a cell range path -# Use autoFilter=true to filter the full used range, or autoFilter=A1:D1 to specify a range -officecli set dashboard.xlsx /Data --prop autoFilter=A1:D1 -``` - -> **Multi-sheet dashboards:** If your dashboard has multiple data sheets (e.g., Data + Expenses, or Sheet1 + KPIs), apply `freeze=A2` and `autoFilter` to **every** data sheet, not just the primary one. Applying these settings only to the primary sheet leaves secondary sheets without header lock and filter — inconsistent and unprofessional. -> -> ```bash -> # Apply to each additional data sheet -> officecli set dashboard.xlsx /Expenses --prop freeze=A2 -> officecli set dashboard.xlsx /Expenses --prop autoFilter=A1:E1 -> officecli set dashboard.xlsx /KPIs --prop freeze=A2 -> officecli set dashboard.xlsx /KPIs --prop autoFilter=A1:C1 -> ``` - -After setup, proceed with the same Step 2–10 workflow as the CSV path. Note the last data row number for formula ranges. - -> **Sheet naming recommendation (Fix-3):** Rename the default `Sheet1` to a semantic name that reflects the data content (e.g., `Data`, `HR_Data`, `Sales_Raw`). This improves readability and makes formula references self-documenting. -> -> ```bash -> officecli set dashboard.xlsx "Sheet1!A1" --prop sheetName="Data" -> ``` -> -> **Note:** After renaming, update ALL formulas that reference the old sheet name. For example, `=SUM(Sheet1!B2:B13)` must become `=SUM(Data!B2:B13)`. This includes Dashboard KPI formulas, chart series references, and CF rules. If you prefer to skip renaming to avoid this update burden, ensure the filename itself is descriptive. - -**Then open in resident mode** before continuing. All subsequent steps (2–10) run in memory: - -```bash -officecli open dashboard.xlsx # (skip if already opened in Option B above) -# ... steps 2–10 ... -officecli close dashboard.xlsx -officecli validate dashboard.xlsx -``` - ---- - -### Step 2: Set Data Sheet Column Widths - -`import --header` does NOT auto-size columns. Always set widths manually. - -**Recommended widths by column type:** - -| Content Type | Width | -|-------------|-------| -| Date (yyyy-mm-dd) | 14 | -| Currency ($#,##0) | 15 | -| Percentage (0.0%) | 12 | -| Short text | 15 | -| Long text | 20-25 | -| Integer / count | 12 | - -**Individual commands:** - -```bash -officecli set dashboard.xlsx '/Sheet1/col[A]' --prop width=14 -officecli set dashboard.xlsx '/Sheet1/col[B]' --prop width=15 -officecli set dashboard.xlsx '/Sheet1/col[C]' --prop width=15 -officecli set dashboard.xlsx '/Sheet1/col[D]' --prop width=15 -officecli set dashboard.xlsx '/Sheet1/col[E]' --prop width=12 -``` - -**Date column fix:** If dates display as serial numbers (e.g., 45662 instead of 2025-01-15), apply a date number format to the **data cell range** (not the column — `numFmt` is not supported on `col[]` targets): - -```bash -# CORRECT: apply numFmt to the data range -officecli set dashboard.xlsx '/Sheet1/A2:A13' --prop numFmt=yyyy-mm-dd - -# WRONG: col[] does not support numFmt -- will fail with "UNSUPPORTED props: numFmt" -# officecli set dashboard.xlsx '/Sheet1/col[A]' --prop numFmt=yyyy-mm-dd -``` - -Common date formats: `yyyy-mm-dd`, `yyyy-mm`, `mm/dd/yyyy`, `mmm yyyy`. - -Adjust the range (`A2:A13`) to match your actual data rows. - -**Batch alternative** (for many columns): - -```bash -officecli set dashboard.xlsx "/Sheet1/col[A]" --prop width=14 -officecli set dashboard.xlsx "/Sheet1/col[B]" --prop width=15 -officecli set dashboard.xlsx "/Sheet1/col[C]" --prop width=15 -officecli set dashboard.xlsx "/Sheet1/col[D]" --prop width=15 -officecli set dashboard.xlsx "/Sheet1/col[E]" --prop width=12 -``` - -**Summary sheet column widths (MANDATORY when a Summary sheet exists):** - -If your dashboard includes a Summary sheet (created in A.2b for multi-dimensional data aggregation), you MUST also set column widths on that sheet. The `col[]` auto-size behavior does NOT apply to formula-populated sheets. - -> **Why this matters:** Summary sheets populated with `SUMIFS`/`AVERAGEIFS` formulas default to column width 8. Text labels (Channel, Department, Category, etc.) truncate; numeric values may show `###`. Always set widths after writing Summary data. - -```bash -# Summary sheet column widths — set after writing all Summary formulas/data -# Text label column (first column: Category, Channel, Department, etc.) -officecli set dashboard.xlsx "/Summary/col[A]" --prop width=18 -# Numeric value columns (adjust count to match actual column count) -officecli set dashboard.xlsx "/Summary/col[B]" --prop width=14 -officecli set dashboard.xlsx "/Summary/col[C]" --prop width=14 -officecli set dashboard.xlsx "/Summary/col[D]" --prop width=14 -# General width guidance: -# Text/label columns (A): 15-20 -# Numeric/value columns (B+): 12-15 -``` - -If Summary has more columns, extend the pattern accordingly. - ---- - -### Step 2b: Header Row Fill Colors (MANDATORY) - -> **QUALITY BAR REQUIREMENT:** Every sheet with column headers MUST have a fill color applied to the header row. Failure to set header fill is a Quality Bar violation (Q2 fail). - -Apply a dark background fill to row 1 of the Data sheet and Summary sheet. This makes the header row immediately distinguishable from data rows. - -**Data sheet header row fill (dark blue with white text):** - -```bash -# Data sheet header row fill -officecli set dashboard.xlsx '/Sheet1/A1:F1' --prop fill=1F3864 --prop font.color=FFFFFF --prop font.bold=true -``` - -Adjust the range (`A1:F1`) to match the actual number of columns in your data sheet. - -**Summary sheet header fill (if Summary sheet exists):** - -```bash -# Summary sheet header fill -officecli set dashboard.xlsx '/Summary/A1:E1' --prop fill=1F3864 --prop font.color=FFFFFF --prop font.bold=true -``` - -Adjust the range to match the actual number of columns in your Summary sheet. - -**Recommended fill colors by tab color theme:** - -| Tab Color Theme | fill | font.color | -|-----------------|------|------------| -| Blue (#4472C4) | `1F3864` | `FFFFFF` | -| Green (#70AD47) | `2E7B32` | `FFFFFF` | -| Gray (#A5A5A5) | `546E7A` | `FFFFFF` | -| Corporate dark | `37474F` | `FFFFFF` | - -**In the Section C example**, the header fill commands appear immediately after Step 2 column widths: - -```bash -# ── Step 2b: Header row fill colors (MANDATORY) ── -officecli set saas_dashboard.xlsx '/Sheet1/A1:E1' --prop fill=1F3864 --prop font.color=FFFFFF --prop font.bold=true -``` - ---- - -### Step 3: Add Dashboard Sheet - -```bash -officecli add dashboard.xlsx / --type sheet --prop name=Dashboard -``` - -> **CRITICAL: Dashboard Architecture — Read Before Step 4** -> -> The Dashboard sheet MUST contain ALL visible content: KPI cards, charts, and the title area. -> Summary/Data sheets store data sources only — users will NOT look at these sheets. -> -> **Correct architecture:** -> ``` -> Dashboard (what users see) -> ← contains: KPI labels + values (rows 1-2) + charts (rows 5+) -> ← all KPI values are formulas referencing Summary or Data sheets -> Summary (aggregated data — hidden from users) -> ← contains: SUMIFS / AVERAGEIFS rollups, helper columns for charts -> Data / Sheet1 (raw data — hidden from users) -> ← contains: imported CSV rows -> ``` -> -> **WRONG:** Putting KPI values only in Summary/Data sheet and leaving Dashboard empty. -> **CORRECT:** KPI label cells (A1, C1, E1, G1) and KPI value cells (A2, C2, E2, G2) MUST be on the Dashboard sheet, with formulas like `=SUM(Sheet1!B2:B13)` pointing to the data sheet. -> -> If you are unsure which sheet a KPI value belongs to, ask: "Will the user see this cell when they open the file?" If yes, it belongs on Dashboard. - ---- - -### Step 4: Build KPI Cells on Dashboard (batch) - -Each KPI is a **label cell** (row 1) + **value cell** (row 2): - -- Label: small gray text (`font.size=9`, `font.color=666666`) -- Value: large bold number with formula and numFmt (`font.size=24`, `bold=true`) - -> **WARNING: Batch JSON values must ALL be strings.** -> CORRECT: `{"bold":"true","font.size":"24","numFmt":"$#,##0"}` -> WRONG: `{"bold":true,"font.size":24}` -- will fail with JSON deserialization error. - -**Template for 4 KPIs:** - -```bash -officecli set dashboard.xlsx /Dashboard/A1 --prop value="Total Revenue" --prop bold=true --prop font.size=9 --prop font.color=666666 -officecli set dashboard.xlsx /Dashboard/A2 --prop "formula==SUM(Sheet1!B2:B13)" --prop numFmt='$#,##0' --prop font.size=24 --prop bold=true --prop font.color=2E7D32 -officecli set dashboard.xlsx /Dashboard/C1 --prop value="Average Monthly" --prop bold=true --prop font.size=9 --prop font.color=666666 -officecli set dashboard.xlsx /Dashboard/C2 --prop "formula==AVERAGE(Sheet1!B2:B13)" --prop numFmt='$#,##0' --prop font.size=24 --prop bold=true --prop font.color=2E7D32 -officecli set dashboard.xlsx /Dashboard/E1 --prop value="Growth Rate" --prop bold=true --prop font.size=9 --prop font.color=666666 -officecli set dashboard.xlsx /Dashboard/E2 --prop "formula==IFERROR((Sheet1!B13-Sheet1!B2)/Sheet1!B2,0)" --prop numFmt=0.0% --prop font.size=24 --prop bold=true --prop font.color=2E7D32 -officecli set dashboard.xlsx /Dashboard/G1 --prop value="Latest Value" --prop bold=true --prop font.size=9 --prop font.color=666666 -officecli set dashboard.xlsx /Dashboard/G2 --prop "formula==Sheet1!B13" --prop numFmt='$#,##0' --prop font.size=24 --prop bold=true --prop font.color=2E7D32 -``` - -**Common KPI formulas:** - -| KPI | Formula | numFmt | -|-----|---------|--------| -| Total | `=SUM(Sheet1!B2:B13)` | `$#,##0` | -| Average | `=AVERAGE(Sheet1!B2:B13)` | `$#,##0` | -| Max | `=MAX(Sheet1!B2:B13)` | `$#,##0` | -| Min | `=MIN(Sheet1!B2:B13)` | `$#,##0` | -| Count | `=COUNT(Sheet1!B2:B13)` | `#,##0` | -| Growth rate | `=IFERROR((Sheet1!B13-Sheet1!B2)/Sheet1!B2,0)` | `0.0%` | -| Average rate | `=AVERAGE(Sheet1!E2:E13)` | `0.0%` | -| Percentage change | `=IFERROR(Sheet1!B13/Sheet1!B12-1,0)` | `0.0%` | - -Always wrap division formulas in `IFERROR` to prevent `#DIV/0!` errors. - ---- - -### Step 4b: Set Dashboard Column Widths (MANDATORY) - -> **CRITICAL: KPI values at font.size=24 + bold + numFmt WILL display as "###" if columns are too narrow.** You MUST set Dashboard column widths after creating KPI cells. This step is NOT optional. - -KPI value columns (A, C, E, G) need width 22 to fit formatted numbers like `$6,320,000` or `0.0%` at 24pt bold. Sparkline columns (B, D, F, H) need width 12. - -**Batch command (recommended):** - -```bash -officecli set dashboard.xlsx "/Dashboard/col[A]" --prop width=22 -officecli set dashboard.xlsx "/Dashboard/col[B]" --prop width=12 -officecli set dashboard.xlsx "/Dashboard/col[C]" --prop width=22 -officecli set dashboard.xlsx "/Dashboard/col[D]" --prop width=12 -officecli set dashboard.xlsx "/Dashboard/col[E]" --prop width=22 -officecli set dashboard.xlsx "/Dashboard/col[F]" --prop width=12 -officecli set dashboard.xlsx "/Dashboard/col[G]" --prop width=22 -officecli set dashboard.xlsx "/Dashboard/col[H]" --prop width=12 -``` - -Adjust the number of columns to match your KPI count. For 5+ KPIs, add columns I and J: - -```bash - {"command":"set","path":"/Dashboard/col[I]","props":{"width":"22"}}, - {"command":"set","path":"/Dashboard/col[J]","props":{"width":"12"}} -``` - -**KPI card background fill (Quality Bar item):** - -KPI cards on the Dashboard sheet SHOULD have a light background fill (e.g., `F0F4FF` for blue tint). This is a Quality Bar item — omitting it is a P2 quality deduction. - -```bash -# Apply a light blue background to the KPI card area (rows 1-2) -officecli set dashboard.xlsx '/Dashboard/A1:H2' --prop fill=F0F4FF -``` - -Adjust the range to match your KPI column layout (e.g., `A1:J2` for 5 KPI pairs). This is separate from the large 24pt KPI value formatting — the fill is applied to the cell background, not the font. - ---- - -### Step 5: Add Sparklines Next to KPIs - -> **DECISION GATE: When to SKIP sparklines** -> - SKIP if data has **fewer than 10 rows** (too few points for a meaningful trend). -> - SKIP if data is **cross-sectional, not time-series** (e.g., regions, departments, products with no time ordering). Sparklines show 1D trends -- they are meaningless for categorical/cross-sectional data. Skip even if the complexity table says "YES" or "Optional." -> - INCLUDE only when rows represent a **sequential time series** (dates, months, quarters) with 10+ data points. - -```bash -# Line sparkline next to KPI 1 -officecli add dashboard.xlsx /Dashboard --type sparkline \ - --prop cell=B2 \ - --prop range="Sheet1!B2:B13" \ - --prop type=line \ - --prop color=4472C4 \ - --prop highpoint=FF0000 - -# Column sparkline next to KPI 2 -officecli add dashboard.xlsx /Dashboard --type sparkline \ - --prop cell=D2 \ - --prop range="Sheet1!C2:C13" \ - --prop type=column \ - --prop color=4472C4 - -# Line sparkline next to KPI 3 -officecli add dashboard.xlsx /Dashboard --type sparkline \ - --prop cell=F2 \ - --prop range="Sheet1!E2:E13" \ - --prop type=line \ - --prop color=2E7D32 - -# Line sparkline next to KPI 4 -officecli add dashboard.xlsx /Dashboard --type sparkline \ - --prop cell=H2 \ - --prop range="Sheet1!B2:B13" \ - --prop type=line \ - --prop color=4472C4 -``` - -Sparkline rules: -- Range must be 1D (single row or single column) -- Cross-sheet ranges work: `range="Sheet1!B2:B13"` -- Sparklines are add-only (cannot modify after creation) - ---- - -### Step 6: Add Charts - -> **STOP -- Multi-Dimensional Data Check:** -> If your data has **repeating values in the X-axis column** (e.g., the same department appearing 4 times for 4 quarters, or the same region appearing across multiple categories), do NOT chart raw rows directly. The chart will have noisy, repeating X-axis labels and be unreadable. -> -> **Go to Section A.2b** and create aggregation formulas on a **dedicated Summary sheet** BEFORE charting. Then reference those Summary sheet cells as chart data sources. -> -> **CRITICAL — Chart data sources must always reference visible cells.** -> Do NOT point `series.values` or `series.categories` to hidden columns, even if those columns contain valid formulas. LibreOffice does not evaluate hidden column formulas at render time, resulting in blank/empty charts. Always use a visible Summary sheet as the intermediary for aggregated chart data. - -#### 6a: Cell Range References (MANDATORY Pattern) - -> **CRITICAL: Always use cell range references, NEVER inline data.** -> CORRECT: `--prop series1.values="Sheet1!B2:B13" --prop series1.categories="Sheet1!A2:A13"` -> WRONG: `--prop series1="Revenue:100,200,300"` -- creates a static chart that cannot update. -> -> Exception: Only use inline data when data requires aggregation that cannot be expressed in Excel formulas (e.g., weekly rollup from daily data). Document the exception explicitly if used. - -#### 6b: Undocumented Chart Properties (DeferredAddKeys) - -These properties work on `add` ONLY. They are NOT in `--help` output. The agent cannot discover them -- they must be specified from this reference. - -| Property | Syntax | When to Use | -|----------|--------|-------------| -| `preset` | `--prop preset=dashboard` | **EVERY chart.** Options: minimal, dark, corporate, magazine, dashboard, colorful, monochrome | -| `referenceline` | `--prop referenceline="value:color:label:dash"` | Budget targets, averages, thresholds | -| `trendline` | `--prop trendline=linear` | Time-series charts to show direction | -| `axisNumFmt` | `--prop axisNumFmt='$#,##0'` | Currency/percentage value axes | - -**Reference line format variants:** -- `"125"` -- value only -- `"125:FF0000"` -- value + color -- `"125:FF0000:Target"` -- value + color + label -- `"125:FF0000:Target:dash"` -- value + color + label + dash style - -> **The format is `value:color:label:dash`** (color BEFORE label). The parser treats the second field as a color. Putting a label in the second field causes `Invalid color value` errors. - -**Trendline types:** `linear`, `exp`, `log`, `poly:N`, `movingAvg:N` - -#### Chart Templates - -> **WARNING: `series1.name=` is MANDATORY on every series.** If you omit the `.name` property, the chart legend will display generic labels like "Series1", "Series2", etc. This is unprofessional and unacceptable for any dashboard. Always set a descriptive name for every series (e.g., `--prop series1.name="Revenue"`). - -**Line chart (time series trend):** - -```bash -officecli add dashboard.xlsx /Dashboard --type chart \ - --prop chartType=line \ - --prop title="Net MRR Trend" \ - --prop series1.name="Net MRR" \ - --prop series1.values="Sheet1!B2:B13" \ - --prop series1.categories="Sheet1!A2:A13" \ - --prop preset=dashboard \ - --prop trendline=linear \ - --prop axisNumFmt='$#,##0' \ - --prop x=0 --prop y=5 --prop width=10 --prop height=15 -``` - -**Column chart (categorical comparison):** - -```bash -officecli add dashboard.xlsx /Dashboard --type chart \ - --prop chartType=column \ - --prop title="Monthly Revenue" \ - --prop series1.name="Revenue" \ - --prop series1.values="Sheet1!B2:B13" \ - --prop series1.categories="Sheet1!A2:A13" \ - --prop preset=dashboard \ - --prop axisNumFmt='$#,##0' \ - --prop x=11 --prop y=5 --prop width=10 --prop height=15 -``` - -**Stacked bar chart (component breakdown):** - -```bash -officecli add dashboard.xlsx /Dashboard --type chart \ - --prop chartType=columnStacked \ - --prop title="MRR Components" \ - --prop series1.name="New MRR" \ - --prop series1.values="Sheet1!B2:B13" \ - --prop series2.name="Expansion" \ - --prop series2.values="Sheet1!C2:C13" \ - --prop series3.name="Churned" \ - --prop series3.values="Sheet1!D2:D13" \ - --prop series1.categories="Sheet1!A2:A13" \ - --prop preset=dashboard \ - --prop axisNumFmt='$#,##0' \ - --prop x=0 --prop y=21 --prop width=10 --prop height=15 -``` - -**Combo chart (budget vs actual):** - -```bash -officecli add dashboard.xlsx /Dashboard --type chart \ - --prop chartType=combo \ - --prop title="Budget vs Actual" \ - --prop series1.name="Budget" \ - --prop series1.values="Sheet1!C2:C13" \ - --prop series2.name="Actual" \ - --prop series2.values="Sheet1!D2:D13" \ - --prop series1.categories="Sheet1!A2:A13" \ - --prop comboSplit=1 \ - --prop preset=dashboard \ - --prop axisNumFmt='$#,##0' \ - --prop x=11 --prop y=5 --prop width=10 --prop height=15 -``` - -**Doughnut chart (composition):** - -> **Note:** Doughnut/pie-style charts show part-of-whole breakdowns. Prefer `doughnut` over `pie` — `chartType=pie` has a known rendering issue in LibreOffice (chart renders blank). Use `doughnut` as the safe alternative. - -```bash -officecli add dashboard.xlsx /Dashboard --type chart \ - --prop chartType=doughnut \ - --prop title="Revenue Breakdown" \ - --prop series1.name="Revenue" \ - --prop series1.values="Sheet1!B2:B5" \ - --prop categories="Sheet1!A2:A5" \ - --prop preset=dashboard \ - --prop holesize=60 \ - --prop axisNumFmt='#,##0' \ - --prop x=11 --prop y=21 --prop width=10 --prop height=15 -``` - -**Scatter chart (correlation):** - -> **IMPORTANT: Scatter charts use `series1.xValues` instead of `series1.categories`.** Using `categories` on a scatter chart produces invalid OOXML. - -```bash -officecli add dashboard.xlsx /Dashboard --type chart \ - --prop chartType=scatter \ - --prop title="Price vs Volume" \ - --prop series1.name="Products" \ - --prop series1.values="Sheet1!C2:C13" \ - --prop series1.xValues="Sheet1!B2:B13" \ - --prop preset=dashboard \ - --prop axisNumFmt='#,##0' \ - --prop x=0 --prop y=21 --prop width=10 --prop height=15 -``` - -**Column chart with reference line:** - -```bash -officecli add dashboard.xlsx /Dashboard --type chart \ - --prop chartType=column \ - --prop title="Revenue vs Target" \ - --prop series1.name="Revenue" \ - --prop series1.values="Sheet1!B2:B13" \ - --prop series1.categories="Sheet1!A2:A13" \ - --prop preset=dashboard \ - --prop axisNumFmt='$#,##0' \ - --prop "referenceline=100000:FF0000:Target:dash" \ - --prop x=0 --prop y=5 --prop width=10 --prop height=15 -``` - -#### 6c: Multi-Series Charts - -Each series needs `.name`, `.values`, and `.categories`: - -```bash ---prop series1.name="Revenue" \ ---prop series1.values="Sheet1!B2:B13" \ ---prop series1.categories="Sheet1!A2:A13" \ ---prop series2.name="Cost" \ ---prop series2.values="Sheet1!C2:C13" \ ---prop series2.categories="Sheet1!A2:A13" -``` - -For combo charts, `comboSplit=N` makes the first N series bars and the rest lines. - -#### 6d: Chart Position Validation (MANDATORY After Adding Each Chart) - -After adding each chart, verify it fits within the Dashboard visible area. - -**Standard Dashboard visible area:** columns A–L (x: 0–21), rows 1–30 (y: 1–30). Charts outside this range may be truncated or invisible in print/export. - -**Print area constraint (MANDATORY when print area is set):** -If a print area has been defined for the Dashboard sheet, all charts MUST fit within it. Charts that overflow the print area are clipped in PDF/print output and appear broken. - -- Determine the print area column width (in column units). For example, a print area of A1:L40 spans 12 columns (x: 0–11). -- Keep each chart's `x + width` ≤ print area right boundary. -- **Recommended:** chart width ≤ 90% of print area width (e.g., if print area is 12 columns wide, max chart width = 10). -- If two charts are placed side-by-side, each chart width must be ≤ 45% of print area width so they both fit. - -**Recommended chart sizes:** - -| Use Case | width | height | Notes | -|----------|-------|--------|-------| -| Standard chart (half-width) | 10 | 15 | Fits two charts side by side | -| Wide chart (full-width) | 20 | 15 | Spans full Dashboard width | -| Compact chart | 8 | 12 | For dashboards with 3+ charts | - -**Verify chart position after adding:** - -```bash -# Check chart anchor and position (replace chart[1] with chart[2] etc.) -officecli get dashboard.xlsx '/Dashboard/chart[1]' --json -``` - -In the JSON output, confirm: -- `x` + `width` ≤ 21 (chart does not extend beyond column L) -- `y` + `height` ≤ 30 (chart does not extend below row 30) -- If a print area is set: `x` + `width` does not exceed the print area's right column boundary - -If a chart exceeds the boundary, remove it and re-add with corrected dimensions: - -```bash -officecli remove dashboard.xlsx '/Dashboard/chart[1]' -# Re-add with adjusted x/y/width/height values -``` - -#### 6f: Chart Error Recovery - -If a chart `add` command fails or produces an unexpected result: - -1. **Check how many charts exist:** - ```bash - officecli query dashboard.xlsx 'chart' - ``` - -2. **Remove a broken or ghost chart** (N is the 1-based chart index): - ```bash - officecli remove dashboard.xlsx '/Dashboard/chart[N]' - ``` - Example: to remove the 2nd chart on Dashboard: `officecli remove dashboard.xlsx '/Dashboard/chart[2]'` - -3. **Retry the chart add** with corrected parameters. - -> Always verify chart count after any chart failure. Failed `add` commands may still create a partial/empty chart object that must be removed before retrying. - ---- - -### Step 7: Add Conditional Formatting - -Four CF types are available. Choose based on the data pattern. - -**Databar** -- magnitude comparison (revenue, spend): - -```bash -officecli add dashboard.xlsx /Sheet1 --type databar \ - --prop sqref=B2:B13 --prop color=4472C4 -``` - -**Colorscale** -- heat map (rates, growth): - -```bash -# 2-color scale (low=red, high=green) -officecli add dashboard.xlsx /Sheet1 --type colorscale \ - --prop sqref=E2:E13 --prop mincolor=FFCDD2 --prop maxcolor=C8E6C9 - -# 3-color scale (red -> white -> green) -officecli add dashboard.xlsx /Sheet1 --type colorscale \ - --prop sqref=D2:D13 \ - --prop mincolor=FFCDD2 --prop midcolor=FFFFFF --prop maxcolor=C8E6C9 -``` - -**Iconset** -- status indicators: - -```bash -officecli add dashboard.xlsx /Sheet1 --type iconset \ - --prop sqref=E2:E13 --prop iconset=3Arrows -``` - -Available icon sets: `3Arrows`, `3ArrowsGray`, `3Flags`, `3TrafficLights1`, `3TrafficLights2`, `3Signs`, `3Symbols`, `3Symbols2`, `4Arrows`, `4ArrowsGray`, `4Rating`, `4RedToBlack`, `4TrafficLights`, `5Arrows`, `5ArrowsGray`, `5Rating`, `5Quarters` - -Use `--prop showvalue=false` to show only icons (hide cell values). - -**Formulacf** -- custom business logic: - -> **WARNING: Do NOT use `font.bold` in formulacf.** Use `fill` + `font.color` only. `font.bold` causes validation errors (`` element in dxf/font is not allowed by OOXML schema). - -```bash -# Green highlight when value is high -officecli add dashboard.xlsx /Sheet1 --type formulacf \ - --prop sqref=B2:B13 \ - --prop 'formula=$B2>=100000' \ - --prop fill=C8E6C9 --prop font.color=2E7D32 - -# Red highlight when value is low -officecli add dashboard.xlsx /Sheet1 --type formulacf \ - --prop sqref=B2:B13 \ - --prop 'formula=$B2<100000' \ - --prop fill=FFCDD2 --prop font.color=C62828 -``` - -**Semantic colors reference:** - -| Meaning | Fill | Font Color | -|---------|------|------------| -| Good / Positive | C8E6C9 | 2E7D32 | -| Bad / Negative | FFCDD2 | C62828 | -| Neutral | F5F5F5 | 666666 | - ---- - -### Step 8: Set Tab Colors - -```bash -officecli set dashboard.xlsx /Dashboard --prop tabColor=4472C4 -officecli set dashboard.xlsx /Sheet1 --prop tabColor=A5A5A5 -``` - ---- - -### Step 9: Polish (Optional, Data-Size-Dependent) - -Apply these when the dataset is large enough (50+ rows) or the audience is executive-level. - -**Gradient fills on KPI cells:** - -```bash -officecli set dashboard.xlsx '/Dashboard/A1' \ - --prop fill=4472C4-1A3B6B --prop font.color=FFFFFF --prop bold=true -``` - -**Cell merge for wider KPI labels:** - -```bash -officecli set dashboard.xlsx '/Dashboard/A1:B1' \ - --prop merge=true --prop value="Total Revenue" \ - --prop bold=true --prop font.size=9 --prop font.color=666666 -``` - -**Dashboard zoom to 85%:** - -```bash -officecli set dashboard.xlsx /Dashboard --prop zoom=85 -``` - -**Named ranges (for 50+ row datasets or C-suite audience):** - -```bash -officecli add dashboard.xlsx / --type namedrange \ - --prop name="TotalRevenue" --prop ref="Sheet1!B2:B13" \ - --prop comment="Monthly revenue data" -``` - ---- - -### Step 10: raw-set -- activeTab and fullCalcOnLoad (ALWAYS LAST) - -> **CRITICAL: These MUST be the last commands, after ALL sheets, charts, CF, sparklines, and named ranges are created.** - -**Command 1: Set Dashboard as active tab.** - -Before setting `activeTab`, verify the exact sheet order (0-based index): - -```bash -# List all sheets in order — count from 0 to find Dashboard's index -officecli query dashboard.xlsx 'sheet' -``` - -Example output: -``` -Sheet1 (index 0) -Summary (index 1) -Dashboard (index 2) -``` - -Use the 0-based index of the Dashboard sheet. In this example, `activeTab="2"`. - -```bash -officecli raw-set dashboard.xlsx /workbook \ - --xpath "//x:sheets" \ - --action insertbefore \ - --xml '' -``` - -`activeTab` is 0-based. If Dashboard is the second sheet (Sheet1=0, Dashboard=1), use `activeTab="1"`. **Always confirm the index with `query 'sheet'` — do NOT guess.** - -**Command 2: Set fullCalcOnLoad.** - -```bash -officecli set dashboard.xlsx / --prop calc.fullCalcOnLoad=true -``` - -Do NOT use `raw-set` to insert `` — it creates duplicate elements. The high-level `set` API handles schema ordering automatically. - ---- - -### Step 11: Validate - -```bash -officecli validate dashboard.xlsx -``` - -Must return zero errors. If errors are found: -- Check for `font.bold` in formulacf -- remove it -- Check calcPr -- use `set / --prop calc.fullCalcOnLoad=true` instead of raw-set -- Check for duplicate bookViews -- raw-set may have been run twice -- Fix the issue and re-validate - ---- - -## Section C: Complete Example -- SaaS MRR Dashboard - -A full, copy-pasteable command sequence for a SaaS MRR dataset with 12 rows and 5 columns: `month`, `new_mrr`, `expansion_mrr`, `churned_mrr`, `net_mrr`. - -Assume the CSV file `saas_mrr.csv` exists with this structure: - -``` -month,new_mrr,expansion_mrr,churned_mrr,net_mrr -2025-01,45000,12000,8000,49000 -2025-02,48000,14000,9000,53000 -2025-03,52000,15000,7500,59500 -2025-04,47000,16000,10000,53000 -2025-05,55000,18000,8500,64500 -2025-06,58000,17000,9500,65500 -2025-07,62000,19000,11000,70000 -2025-08,60000,20000,10500,69500 -2025-09,65000,22000,9000,78000 -2025-10,68000,21000,12000,77000 -2025-11,72000,24000,10000,86000 -2025-12,75000,25000,11500,88500 -``` - -**Data analysis:** 12 rows, 5 columns. Primary dimension: month (date). Numeric columns: 4 (all currency). Per the complexity table: 2-3 KPIs, 2 charts, optional sparklines, 1-2 CF rules. - -**Plan:** -- KPIs: Latest Net MRR, MoM Growth Rate, Average Churn Rate, Total Net New MRR -- Charts: Line chart (Net MRR trend with trendline), Stacked column (MRR components) -- Sparklines: 4 (one per KPI) -- CF: Databar on net_mrr, colorscale on churned_mrr - -### Commands - -```bash -# ── Step 1: Create and import ── -officecli create saas_dashboard.xlsx -officecli import saas_dashboard.xlsx /Sheet1 --file saas_mrr.csv --header - -# ── Step 2: Column widths ── -officecli set saas_dashboard.xlsx "/Sheet1/col[A]" --prop width=14 -officecli set saas_dashboard.xlsx "/Sheet1/col[B]" --prop width=15 -officecli set saas_dashboard.xlsx "/Sheet1/col[C]" --prop width=15 -officecli set saas_dashboard.xlsx "/Sheet1/col[D]" --prop width=15 -officecli set saas_dashboard.xlsx "/Sheet1/col[E]" --prop width=15 - -# ── Step 3: Add Dashboard sheet ── -officecli add saas_dashboard.xlsx / --type sheet --prop name=Dashboard - -# ── Step 4: Build KPI cells ── -officecli set saas_dashboard.xlsx /Dashboard/A1 --prop value="Latest Net MRR" --prop bold=true --prop font.size=9 --prop font.color=666666 -officecli set saas_dashboard.xlsx /Dashboard/A2 --prop "formula==Sheet1!E13" --prop numFmt='$#,##0' --prop font.size=24 --prop bold=true --prop font.color=2E7D32 -officecli set saas_dashboard.xlsx /Dashboard/C1 --prop value="MoM Growth" --prop bold=true --prop font.size=9 --prop font.color=666666 -officecli set saas_dashboard.xlsx /Dashboard/C2 --prop "formula==IFERROR(Sheet1!E13/Sheet1!E12-1,0)" --prop numFmt=0.0% --prop font.size=24 --prop bold=true --prop font.color=2E7D32 -officecli set saas_dashboard.xlsx /Dashboard/E1 --prop value="Avg Churn Rate" --prop bold=true --prop font.size=9 --prop font.color=666666 -officecli set saas_dashboard.xlsx /Dashboard/E2 --prop "formula==IFERROR(AVERAGE(Sheet1!D2:D13)/AVERAGE(Sheet1!E2:E13),0)" --prop numFmt=0.0% --prop font.size=24 --prop bold=true --prop font.color=C62828 -officecli set saas_dashboard.xlsx /Dashboard/G1 --prop value="Total Net New MRR" --prop bold=true --prop font.size=9 --prop font.color=666666 -officecli set saas_dashboard.xlsx /Dashboard/G2 --prop "formula==SUM(Sheet1!E2:E13)" --prop numFmt='$#,##0' --prop font.size=24 --prop bold=true --prop font.color=2E7D32 - -# ── Step 4b: Dashboard column widths (MANDATORY -- prevents ### on KPIs) ── -officecli set saas_dashboard.xlsx "/Dashboard/col[A]" --prop width=22 -officecli set saas_dashboard.xlsx "/Dashboard/col[B]" --prop width=12 -officecli set saas_dashboard.xlsx "/Dashboard/col[C]" --prop width=22 -officecli set saas_dashboard.xlsx "/Dashboard/col[D]" --prop width=12 -officecli set saas_dashboard.xlsx "/Dashboard/col[E]" --prop width=22 -officecli set saas_dashboard.xlsx "/Dashboard/col[F]" --prop width=12 -officecli set saas_dashboard.xlsx "/Dashboard/col[G]" --prop width=22 -officecli set saas_dashboard.xlsx "/Dashboard/col[H]" --prop width=12 - -# ── Step 5: Sparklines ── -officecli add saas_dashboard.xlsx /Dashboard --type sparkline \ - --prop cell=B2 \ - --prop range="Sheet1!E2:E13" \ - --prop type=line \ - --prop color=4472C4 \ - --prop highpoint=FF0000 - -officecli add saas_dashboard.xlsx /Dashboard --type sparkline \ - --prop cell=D2 \ - --prop range="Sheet1!E2:E13" \ - --prop type=column \ - --prop color=4472C4 - -officecli add saas_dashboard.xlsx /Dashboard --type sparkline \ - --prop cell=F2 \ - --prop range="Sheet1!D2:D13" \ - --prop type=line \ - --prop color=C62828 - -officecli add saas_dashboard.xlsx /Dashboard --type sparkline \ - --prop cell=H2 \ - --prop range="Sheet1!E2:E13" \ - --prop type=line \ - --prop color=2E7D32 - -# ── Step 6: Charts ── - -# Line chart: Net MRR trend with trendline (left position) -officecli add saas_dashboard.xlsx /Dashboard --type chart \ - --prop chartType=line \ - --prop title="Net MRR Trend" \ - --prop series1.name="Net MRR" \ - --prop series1.values="Sheet1!E2:E13" \ - --prop series1.categories="Sheet1!A2:A13" \ - --prop preset=dashboard \ - --prop trendline=linear \ - --prop axisNumFmt='$#,##0' \ - --prop x=0 --prop y=5 --prop width=10 --prop height=15 - -# Stacked column: MRR components breakdown (right position) -officecli add saas_dashboard.xlsx /Dashboard --type chart \ - --prop chartType=columnStacked \ - --prop title="MRR Components" \ - --prop series1.name="New MRR" \ - --prop series1.values="Sheet1!B2:B13" \ - --prop series2.name="Expansion" \ - --prop series2.values="Sheet1!C2:C13" \ - --prop series3.name="Churned" \ - --prop series3.values="Sheet1!D2:D13" \ - --prop series1.categories="Sheet1!A2:A13" \ - --prop preset=dashboard \ - --prop axisNumFmt='$#,##0' \ - --prop x=11 --prop y=5 --prop width=10 --prop height=15 - -# ── Step 7: Conditional formatting ── - -# Databar on net_mrr column -officecli add saas_dashboard.xlsx /Sheet1 --type databar \ - --prop sqref=E2:E13 --prop color=4472C4 - -# Colorscale on churned_mrr (red=high churn, green=low) -officecli add saas_dashboard.xlsx /Sheet1 --type colorscale \ - --prop sqref=D2:D13 --prop mincolor=C8E6C9 --prop maxcolor=FFCDD2 - -# ── Step 8: Tab colors ── -officecli set saas_dashboard.xlsx /Dashboard --prop tabColor=4472C4 -officecli set saas_dashboard.xlsx /Sheet1 --prop tabColor=A5A5A5 - -# ── Step 9: Polish (skip for 12-row dataset -- optional) ── - -# ── Step 10: raw-set (LAST) ── - -# Set Dashboard as active tab -officecli raw-set saas_dashboard.xlsx /workbook \ - --xpath "//x:sheets" \ - --action insertbefore \ - --xml '' - -# Set fullCalcOnLoad (use high-level API, not raw-set) -officecli set saas_dashboard.xlsx / --prop calc.fullCalcOnLoad=true - -# ── Step 11: Close + Validate ── -officecli close saas_dashboard.xlsx -officecli validate saas_dashboard.xlsx -``` - ---- - -## Section D: Warnings and Known Issues - -> **Read these before building. Each one has caused production failures.** - -### D-1: Batch JSON -- All Values Must Be Strings - -```json -CORRECT: {"bold":"true","font.size":"24","numFmt":"$#,##0"} -WRONG: {"bold":true,"font.size":24} -``` - -Non-string values fail with `JSON value could not be converted to System.String`. This applies to ALL batch prop values including booleans and numbers. - -### D-2: DeferredAddKeys -- Add-Only, Not in --help - -`preset`, `referenceline`, `trendline`, and `axisNumFmt` are NOT listed in `officecli --help` output. They only work on `add` commands, NOT on `set`. Always include them at chart creation time. You cannot apply a preset to an existing chart. - -### D-3: raw-set Ordering -- activeTab LAST - -The `raw-set` command for `activeTab` MUST be the last raw-set command in the workflow, after all sheets, charts, CF rules, and sparklines are created. Setting `activeTab` before all sheets exist will produce wrong indices. - -### D-4: calcPr -- Use High-Level API - -Use `officecli set file.xlsx / --prop calc.fullCalcOnLoad=true` instead of `raw-set` to set calculation properties. The `raw-set` approach creates duplicate `` elements and causes validation errors. - -### D-5: formulacf -- No font.bold - -`font.bold` in formulacf causes validation error: `unexpected child element in dxf/font`. Use `fill` + `font.color` only for formula-based conditional formatting. - -### D-6: Column Widths -- import Does Not Auto-Size - -`import --header` sets freeze pane and AutoFilter but does NOT adjust column widths. Always set widths manually after import (Step 2). - -### D-7: Shell Quoting -- Formulas with $ and ! - -- `$` in formulas: use single quotes to prevent shell expansion: `--prop 'formula=$D2>$C2'` -- `!` in cross-sheet references: use double quotes or batch/heredoc: `--prop "formula==SUM(Sheet1!B2:B13)"` -- For batch JSON, use heredoc with single-quoted delimiter: `cat <<'EOF' | officecli batch` - -### D-8: Scatter Charts -- Use xValues, Not Categories - -Scatter charts use a different OOXML structure than other chart types. They require `` (X values) instead of `` (categories). In officecli: - -- **CORRECT:** `--prop series1.xValues="Sheet1!A2:A13"` -- **WRONG:** `--prop series1.categories="Sheet1!A2:A13"` -- produces `` element which is invalid in `` and fails validation. - -### D-9: Reference Line Format -- Color Before Label - -The reference line format is `value:color:label:dash`, NOT `value:label:color:dash`. The parser always treats the second colon-delimited field as a color value. - -- **CORRECT:** `--prop "referenceline=0:FF0000:Break-Even:dash"` -- **WRONG:** `--prop "referenceline=0:Break-Even:FF0000:dash"` -- `Break-Even` is parsed as a color and fails. - -### D-10: Chart Data -- Always Use Cell Range References - -Inline data (`series1="Revenue:100,200,300"`) creates a static chart. Cell range references (`series1.values="Sheet1!B2:B13"`) create live links. Always prefer cell range references unless data requires aggregation impossible in Excel formulas. - -### D-11: CF Rules Are Add-Only - -Conditional formatting rules cannot be modified or removed after creation. Plan CF rules carefully before adding them. For overlapping ranges, the last rule added takes priority. - -### D-12: SUMIFS with Date Criteria -- Do NOT Use Strings - -When data contains a date column, `SUMIFS` criteria must NOT be plain strings like `"2025-01-05"`. Excel stores dates as serial numbers internally, so a string comparison will silently match zero rows. - -- **CORRECT:** `=SUMIFS(B2:B13,A2:A13,DATE(2025,1,5))` or `=SUMIFS(B2:B13,A2:A13,DATEVALUE("2025-01-05"))` -- **WRONG:** `=SUMIFS(B2:B13,A2:A13,"2025-01-05")` -- returns 0 because the string does not match the numeric date value. - -This also applies to `AVERAGEIFS`, `COUNTIFS`, and any other `*IFS` function when the criteria column contains dates. - -### D-13: Empty Charts -- officecli Silently Accepts Missing Data - -`officecli add --type chart` succeeds (exit 0) even when data params (`series1.values=` or `data=`) are omitted entirely. The result is a chart XML structure with no data -- it renders as a blank box in Excel/WPS. **Always verify chart data after creation** using `get --json` (see QA Checklist step 5b). This bug caused blank charts across 3 rounds of testing before detection. - -### D-14: Summary Sheet Percentage Formulas -- Missing numFmt Displays as Float - -When `AVERAGEIFS`, `SUMIFS/count`, or any formula on the Summary sheet produces a percentage value (e.g., conversion rate, churn rate, win rate), the result is stored internally as a decimal (0.0983333...). Without `numFmt`, the cell displays as a raw float in the spreadsheet, not a percentage. - -- **CORRECT:** Set `numFmt` at the same time as the formula: - ```bash - officecli set dashboard.xlsx "Summary!C2" --prop "formula==AVERAGEIFS(Data!C2:C100,Data!A2:A100,B2)" --prop numFmt="0.0%" - ``` -- **WRONG:** Setting only the formula: the cell shows `0.098` instead of `9.8%`. - -This applies to ALL formula cells on Summary sheets where the result is a ratio or percentage. Always check Summary sheet percentage columns in QA. - -### D-15: Summary Sheet Column Widths -- Formulas Do Not Auto-Size - -Like the Data sheet, Summary sheet columns are NOT auto-sized when populated with formulas. Text label columns default to width 8 and will truncate category names. Numeric columns may show `###` for large values. - -Always run column width commands after writing Summary sheet data (see Step 2 — Summary sheet column widths section). - ---- - -## Section E: QA Checklist - -Run this checklist after every dashboard build. Do not skip any step. - -### Automated Checks - -```bash -# 1. Validation -- must return zero errors -officecli validate dashboard.xlsx - -# 2. Issue detection -officecli view dashboard.xlsx issues - -# 3. Verify EVERY KPI cell has a formula (not hardcoded values) -# Check each KPI value cell individually. The "formula" field must be present. -officecli get dashboard.xlsx /Dashboard/A2 --json -officecli get dashboard.xlsx /Dashboard/C2 --json -officecli get dashboard.xlsx /Dashboard/E2 --json -officecli get dashboard.xlsx /Dashboard/G2 --json -# (add more lines for additional KPI cells, e.g., /Dashboard/I2) - -# 4. Check for formula errors -officecli query dashboard.xlsx 'cell:contains("#REF!")' -officecli query dashboard.xlsx 'cell:contains("#DIV/0!")' -officecli query dashboard.xlsx 'cell:contains("#VALUE!")' -officecli query dashboard.xlsx 'cell:contains("#NAME?")' -officecli query dashboard.xlsx 'cell:contains("#N/A")' - -# 5. Verify chart count matches plan -officecli query dashboard.xlsx 'chart' - -# 5b. **CRITICAL: Verify EVERY chart has data (not empty)** -# For EACH chart, run `get --json` and confirm `series[].values` or `series[].valuesRef` is NOT empty. -# An empty chart (no data) is a BLOCKER -- the chart renders as a blank box. -officecli get dashboard.xlsx '/Dashboard/chart[1]' --json -# Check output: each series MUST have non-empty "values" (inline) OR "valuesRef" (cell range). -# NOTE: When using cell range references (series1.values="Sheet1!B2:B13"), the "values" field -# will always be empty -- this is NORMAL. Only "valuesRef" will be populated. -# BLOCKER: If BOTH "values" AND "valuesRef" are empty → chart has no data. -# FIX: remove the chart and re-add with correct series1.values="Sheet1!B2:B13" params. -# Repeat for chart[2], chart[3], etc. - -# 6. Verify conditional formatting exists (required for datasets with 10+ rows) -officecli query dashboard.xlsx 'conditionalformatting' -# Must return at least 1 rule applied to the Data sheet. -# Zero CF rules on a 10+ row dataset is a quality failure (Q4 fail). - -# 7. Spot-check a cross-sheet formula (no backslash before !) -officecli get dashboard.xlsx /Dashboard/A2 --json - -# 8. Visual content verification (MANDATORY) -# View Dashboard content as text to confirm KPIs display actual values (not ###): -officecli view dashboard.xlsx text -# Verify: -# - KPI values are real numbers (e.g., $88,500 or 14.3%), NOT "###" -# - Dates display as dates (e.g., 2025-01), NOT serial numbers (e.g., 45658) -# - Charts are listed with correct titles -# NOTE: Formula cells (KPIs) may appear BLANK in text view. This is NORMAL -# because `view text` does not execute formula calculations. Blank formula -# cells do NOT indicate missing data. Confirm the formula exists by checking -# step 3 above (`get --json` shows the "formula" field). -``` - -### Manual Verification (Agent Self-Check) - -- [ ] KPI count matches the plan from Section A -- [ ] Chart count matches the plan from Section A -- [ ] **Every chart has non-empty data** (verified via `get --json`: series[].values or series[].valuesRef present) -- [ ] Every chart has `preset=dashboard` (or `corporate`/`minimal` per plan) -- [ ] Every chart has a descriptive title (not "Chart 1") -- [ ] Every chart series has a name (not "Series 1") -- [ ] **Header row fill color applied (Q2 — MANDATORY):** Data sheet row 1 and Summary sheet row 1 have a non-white fill color. Dark fill (e.g., `1F3864`) with white font required. -- [ ] **Conditional formatting applied (Q4 — MANDATORY for 10+ row datasets):** At least 1 CF rule (colorscale or databar) present on a numeric column in the Data sheet. Verify with `officecli query dashboard.xlsx 'conditionalformatting'` — zero rules is a quality failure. -- [ ] **KPI card background fill applied (Q3 — Quality Bar):** Dashboard KPI area (rows 1-2) has a light background fill (e.g., `F0F4FF`). Omitting this is a P2 deduction. -- [ ] CF rules use correct color direction (green=good, red=bad) -- [ ] Tab colors are set (Dashboard=blue, data=gray) -- [ ] Dashboard is active on open (activeTab set) -- [ ] fullCalcOnLoad is set -- [ ] Output complexity is proportional to data size (no 3 charts for 5 rows) -- [ ] **Summary sheet column widths set** (if Summary sheet exists): text/label columns 15-20, numeric columns 12-15 (see Step 2 Summary section) -- [ ] **Summary sheet percentage formulas have numFmt** (if applicable): cells with AVERAGEIFS/rate results must have `numFmt="0.0%"` or `"0%"`, not raw float display -- [ ] **Chart data sources reference visible cells only:** No chart `series.values` or `series.categories` points to a hidden column or hidden sheet. Hidden-column formula results are not evaluated by LibreOffice — charts referencing them render blank. Use visible Summary sheet cells as chart data sources. -- [ ] **Charts within print area (if print area is set):** For each chart, confirm `x + width` does not exceed the print area's right boundary. Recommended: chart width ≤ 90% of print area width. Side-by-side charts: each ≤ 45% of print area width. diff --git a/skills/officecli-docx/SKILL.md b/skills/officecli-docx/SKILL.md index b56ca320b..a4399e0a2 100644 --- a/skills/officecli-docx/SKILL.md +++ b/skills/officecli-docx/SKILL.md @@ -1,413 +1,661 @@ --- -# officecli: v1.0.23 name: officecli-docx description: "Use this skill any time a .docx file is involved -- as input, output, or both. This includes: creating Word documents, reports, letters, memos, or proposals; reading, parsing, or extracting text from any .docx file; editing, modifying, or updating existing documents; working with templates, tracked changes, comments, headers/footers, or tables of contents. Trigger whenever the user mentions 'Word doc', 'document', 'report', 'letter', 'memo', or references a .docx filename." --- -# officecli: v1.0.23 # OfficeCLI DOCX Skill -## BEFORE YOU START (CRITICAL) +## Setup -**Every time before using officecli, run this check:** +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. + +## ⚠️ Help-First Rule + +**This skill teaches what good docx looks like, not every command flag. When a property name, enum value, or alias is uncertain, consult help BEFORE guessing.** ```bash -if ! command -v officecli &> /dev/null; then - echo "Installing officecli..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - # Windows: irm https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.ps1 | iex -else - CURRENT=$(officecli --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - LATEST=$(curl -fsSL https://api.github.com/repos/iOfficeAI/OfficeCLI/releases/latest | grep '"tag_name"' | sed -E 's/.*"v?([0-9.]+)".*/\1/') - if [ "$CURRENT" != "$LATEST" ]; then - echo "Upgrading officecli $CURRENT → $LATEST..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - else - echo "officecli $CURRENT is up to date" - fi -fi -officecli --version +officecli help docx # List all docx elements +officecli help docx # Full element schema (e.g. paragraph, field, numbering, watermark, toc) +officecli help docx # Verb-scoped (e.g. add field, set section) +officecli help docx --json # Machine-readable schema ``` ---- -# officecli: v1.0.23 +Help is pinned to the installed CLI version. When this skill and help disagree, **help is authoritative**. Special-topic mini-sections below end with an explicit pointer back to help. -## Quick Reference +## Mental Model & Inheritance -| Task | Action | -|------|--------| -| Read / analyze content | Use `view` and `get` commands below | -| Edit existing document | Read [editing.md](editing.md) | -| Create from scratch | Read [creating.md](creating.md) | +**Mental model.** A `.docx` is a ZIP of XML parts (`document.xml`, `styles.xml`, `numbering.xml`, `header*.xml`, `footer*.xml`, `comments.xml`, ...). Everything the user sees — headings, tables, page numbers, TOC, tracked changes — is XML inside that ZIP. `officecli` gives you a semantic-path API (`/body/p[1]/r[2]`) over it, so you almost never touch raw XML; when you must, use `raw-set`. ---- -# officecli: v1.0.23 +## Shell & Execution Discipline -## Execution Model +**Shell quoting (zsh / bash).** docx paths contain `[]`, some prop values contain `$`. Both are shell metacharacters. Rules: -**Run commands one at a time. Do not write all commands into a shell script and execute it as a single block.** +- ALWAYS quote element paths: `"/body/p[1]"`, not `/body/p[1]`. +- Use **single quotes** for any prop value containing `$`: `--prop text='$50M'`. The rule holds at any length — a 200-word body paragraph containing `$50M` needs the whole value inside single quotes, same as a three-word heading: `--prop text='In Q4 we hit $50M ARR, up 18% YoY — the strongest quarter since inception...'`. Mixing `'... $var ...'` and `"... $50 ..."` on long strings is where shell-leak silently strips `$50` → nothing. +- NEVER hand-write `\$`, `\t`, `\n` inside executable examples. The CLI does not interpret backslash escapes; they will land in your file as literal characters. In a cell / paragraph text, a real newline goes through the JSON layer (`batch` heredoc with `"\n"` inside the JSON string). -OfficeCLI is incremental: every `add`, `set`, and `remove` immediately modifies the file and returns output. Use this to catch errors early: +**Incremental execution.** Run commands one at a time and read each exit code. `officecli` mutates the file on every call; a 50-command script that fails at command 3 will cascade silently. One command → check output → continue. After any structural op (new style, table, TOC, section break) run `get` on it before stacking more on top. -1. **One command at a time, then read the output.** Check the exit code before proceeding. -2. **Non-zero exit = stop and fix immediately.** Do not continue building on a broken state. -3. **Verify after structural operations.** After adding a style, table, chart, or section, run `get` or `validate` before building on top of it. +**File-name convention in this skill.** All commands use `"$FILE"` — set once at the top of your script or session (`FILE="your-doc.docx"`) and every command picks it up. Copy-paste blocks and individual examples both assume `$FILE` is set. Do NOT copy a literal `doc.docx` / `review.docx` into an output directory — that is the wrong filename, always substitute your actual target. -Running a 50-command script all at once means the first error cascades silently through every subsequent command. Running incrementally means the failure context is immediate and local — fix it and move on. +## Requirements for Outputs ---- -# officecli: v1.0.23 +Before reaching for a command, know what a good docx looks like. These are the deliverable standards every document MUST meet. + +### All documents + +**Clear hierarchy.** Every non-trivial document has Title → Heading 1 → Heading 2 → body, not a wall of unstyled `Normal` paragraphs. A reader scans headings first. If `view outline` shows one flat list of paragraphs, the hierarchy is missing. + +**Explicit heading sizes.** Do NOT rely on Word default style sizes — they drift between templates. Set sizes explicitly: **H1 = 18pt minimum (20pt preferred for long reports)**, H2 = 14pt bold, H3 = 12pt bold. Body = 11-12pt. Line spacing 1.15-1.5x. + +**One body font, one accent.** Pick one readable body font (Calibri, Cambria, Georgia, Times New Roman) and keep it consistent. Accent color for heading emphasis or table headers — not rainbow formatting. + +**Spacing through properties, not empty paragraphs.** Use `spaceBefore` / `spaceAfter` on paragraphs. Rows of empty paragraphs render as spacing in Word but break pagination and `view issues` will flag them. + +**Smart quotes and typographic quality.** New content uses curly quotes (`'`, `'`, `"`, `"`) not ASCII `'` and `"`. Use Unicode directly (`'smart'`) or the XML entities `‘` / `’` / `“` / `”` inside `raw-set`. En-dash `–` for ranges (`2024–2026`), em-dash `—` for parenthetical breaks. + +**Headers, footers, page numbers on any document > 1 page.** Page numbers go through a live `PAGE` field, not the literal text "Page 1". Use `--prop field=page` on a footer add — the CLI injects `` for you (see Creating & Editing → Headers & Footers). + +**Preserve existing templates.** When editing a file that already has a look, match it. Existing conventions override these guidelines. + +### Visual delivery floor (applies to EVERY document) + +Before you declare done, run `officecli view "$FILE" html` and Read the returned HTML path to confirm all of these: -## Reading & Analyzing +- **No placeholder tokens rendered as data.** `$xxx$`, `{var}`, `{{name}}`, ``, `lorem`, `xxxx` must never appear in a heading, body paragraph, cover page, TOC, caption, header, or footer. These are build-time tokens that escaped replacement. If you want a literal `{name}` in a template for a human to fill, wrap it in a visible instruction paragraph ("Replace `{name}` before sending") so no one confuses it with finished content. +- **No truncated titles or overflowing cells.** Long headings / table cell values must fit the page and the column. If a cell overflows, widen the column or set `wrapText` on the cell. +- **Page numbers render as real numbers.** Confirm `get --depth 3` on the footer shows `` children — not just a run with literal text `"Page"`. The footer must contain a live field, not a static word. +- **TOC present when document has 3+ headings.** Add with `--type toc`. The TOC is a live field — some viewers show the heading list immediately, others show `Update field to see table of contents` until the user recalculates (F9 in Word). +- **Cover page ≥ 60% filled, last page ≥ 40% filled.** A cover that is 80% blank space looks unfinished. Pad with subtitle / author / date / scope statement / key highlights / decorative band. A last page with just "Thank you" centered also reads as unfinished — add conclusion, next steps, contact, legal notice. +- **No `\$`, `\t`, `\n` literals in document text.** If you see these in `view text`, a shell-escape layer leaked. Delete the paragraph and re-enter it. -### Text Extraction +If any of the above fails, STOP and fix before declaring done. + +### Hard rules worth repeating (they are how docx goes wrong) + +- Single-command footer with page number: `add / --type footer --prop field=page ...` — do NOT pass `--prop fldChar=...` or hand-compose the field. The CLI handles it. +- First-page footer `--type footer --prop type=first --prop text=""` automatically triggers `differentFirstPage`. Do NOT `set / --prop differentFirstPage=true` separately — that prop is UNSUPPORTED and silently fails. +- TOC add: `--type toc --prop levels="1-3" --prop hyperlinks=true --index 0`. Do NOT pass `--prop pagenumbers=true` — UNSUPPORTED (page numbers render automatically). + +## Common Workflow + +Six steps. Every non-trivial build follows this shape. + +1. **Choose the mode.** Always use `officecli open ` at the start and `officecli close ` at the end. Resident mode is the default, not an optimization — it avoids re-parsing the XML on every command. For many paragraphs of the same style, use `batch` (≤ 12 ops per block for reliability). +2. **Orient.** For a new file, `officecli create "$FILE"`. For existing, `officecli view "$FILE" outline` first — get the heading tree, section count, whether a TOC / watermark / tracked changes are already there. Never start editing blind. +3. **Build incrementally.** Structural first, content next, formatting last. Styles and numbering defs → sections / page setup → headings and body → tables / images / fields / TOC → headers / footers → comments. After each structural op, `get` it back to confirm shape before stacking on top. +4. **Format to spec.** Explicit heading sizes, spacing, widths, alignment, tabs, list indents. Formatting is not optional polish — per Requirements for Outputs it is part of the deliverable. +5. **Close, then recalculate fields.** `officecli close "$FILE"` writes XML to disk. TOC / PAGE / NUMPAGES / SEQ / PAGEREF fields have **cached values** that may be stale or empty. When a human opens the file in Word, they press F9 to recalc. For the CLI's purposes, confirm fields *exist* (via `get --depth 3` finding ``) rather than trusting the text value — the text is the cached render, the field is the truth. +6. **QA — assume there are problems.** See the QA section. You are not done when your last command exited 0; you are done after one fix-and-verify cycle finds zero new issues. + +## Quick Start + +Minimal viable docx: a heading, a body paragraph, a subheading, and a footer with a live page-number field. Adapt, don't copy-paste — your file, your content. ```bash -officecli view doc.docx text -officecli view doc.docx text --max-lines 200 -officecli view doc.docx text --start 1 --end 50 +FILE="review.docx" +officecli create "$FILE" +officecli open "$FILE" +officecli add "$FILE" /body --type paragraph --prop text="Q4 2026 Review" --prop style=Heading1 --prop size=20pt --prop bold=true --prop spaceAfter=12pt +officecli add "$FILE" /body --type paragraph --prop text="Revenue grew 18% year-over-year, ahead of plan." --prop size=11pt --prop spaceAfter=8pt +officecli add "$FILE" /body --type paragraph --prop text="Key Drivers" --prop style=Heading2 --prop size=14pt --prop bold=true --prop spaceBefore=12pt --prop spaceAfter=6pt +officecli add "$FILE" /body --type paragraph --prop text="Enterprise renewals, upsell, and a new EMEA region." --prop size=11pt +officecli add "$FILE" / --type footer --prop type=default --prop size=9pt --prop text="Page " --prop field=page +officecli set "$FILE" "/footer[1]/p[1]" --prop align=center +officecli close "$FILE" +officecli validate "$FILE" ``` -`text` mode shows `[/body/p[N]] text content`, tables as `[Table: N rows]`, equations as readable math. Use `--max-lines` or `--start`/`--end` for large documents to avoid dumping entire content. +Verified: `validate` returns `no errors found`; `get /footer[1] --depth 3` shows the 5-run PAGE field chain (the begin / instrText / separate / cached value / end runs that wrap the live field), not a static `"Page"` string; for the raw `` XML behind those runs, use `officecli raw "$FILE" "/footer[1]" | grep fldChar`. This is the shape of every build: open → structure → content → format → footer/fields → close → validate. + +## Reading & Analysis + +Start wide, then narrow. `outline` tells you what structure is already there; jump into `view text` / `get` / `query` only once you know where to look. -### Structure Overview +**Open the rendered document to eyeball your own work.** +- `officecli view $FILE html` — Read the returned HTML to audit the rendered output. Headings, tables, page breaks visible. Catches heading hierarchy issues, empty paragraphs-as-spacing, missing TOC entries. +- `officecli watch $FILE` keeps a live preview running for the human user — they can open it at their own discretion. Use only when the user wants to watch along; agent self-check uses `view html` above. +Use `view html` as your **first visual check after a batch of edits**. For final visual verification, the user opens the `.docx` in their Word / WPS / Pages viewer. + +**Orient.** Heading tree, section count, table / image counts, watermark, tracked changes presence. ```bash -officecli view doc.docx outline +officecli view "$FILE" outline ``` -Output shows file stats (paragraph count, tables, images, equations), watermark presence, headers/footers, and heading hierarchy tree. +**Extract text for content QA or LLM context.** Paths are shown as `[/body/p[N]]` so you can jump back with `get`. Scope with `--start` / `--end` / `--max-lines` on long documents. + +```bash +officecli view "$FILE" text --start 1 --end 80 +officecli view "$FILE" annotated # values + style/font/size + warnings per run +officecli view "$FILE" stats # paragraph counts, font usage, style distribution +officecli view "$FILE" issues # empty paras, missing alt text, spacing anomalies +``` -### Detailed Inspection +**Inspect one element.** XPath-style semantic paths (1-based, like XPath). Always quote — shells glob `[N]`. ```bash -officecli view doc.docx annotated +officecli get "$FILE" / # document root: metadata, page setup +officecli get "$FILE" /body --depth 1 # body children overview +officecli get "$FILE" "/body/p[1]" # one paragraph +officecli get "$FILE" "/body/p[1]/r[1]" # one run (character-level formatting) +officecli get "$FILE" "/body/tbl[1]" --depth 3 # table with rows and cells +officecli get "$FILE" "/footer[1]" --depth 3 # footer — check for fldChar +officecli get "$FILE" "/styles/Heading1" # style definition +officecli get "$FILE" /numbering --depth 2 # numbering abstractNum + num bindings ``` -Shows style, font, size, bold/italic per run; equations as LaTeX; images with alt text. Empty paragraphs shown as `[] <-- empty paragraph`. +Add `--json` for machine output. Use `[last()]` (with parentheses) to address the last element: `/body/tbl[last()]/tr[1]`. `[last]` without parens errors. -### Statistics +**Query across the document.** CSS-like selectors, for systematic checks rather than hand-walking. ```bash -officecli view doc.docx stats +officecli query "$FILE" 'paragraph[style=Heading1]' # all H1s +officecli query "$FILE" 'p:contains("quarterly")' # text match +officecli query "$FILE" 'p:empty' # empty paragraphs (clutter) +officecli query "$FILE" 'image:no-alt' # accessibility gaps +officecli query "$FILE" 'paragraph[size>=24pt]' # numeric comparison +officecli query "$FILE" 'field[fieldType!=page]' # fields other than PAGE ``` -Paragraph count, style distribution, font usage, font size usage, empty paragraph count. +Operators: `=`, `!=`, `~=` (contains), `>=`, `<=`, `[attr]` (exists). Full selector reference: `officecli query --help`. -### Element Inspection +**Large documents.** When a document is long enough that `view text` is unwieldy, use `view outline` to navigate by heading and `query` to jump directly to what you need — don't dump the whole body into context. -```bash -# Document root (metadata, page setup) -officecli get doc.docx / +## Creating & Editing -# List body children at depth 1 -officecli get doc.docx /body --depth 1 +The verbs: `add` (new element), `set` (change a prop), `remove`, `move`, `swap`, `batch`, `raw-set` (last-resort XML). Ninety percent of a docx build is paragraphs, runs, tables, a couple of images, a TOC, and a footer. -# Specific paragraph -officecli get doc.docx "/body/p[1]" +### Paragraphs, runs, styles -# Specific run -officecli get doc.docx "/body/p[1]/r[1]" +A paragraph (`p`) is a block; a run (`r`) is a span of consistent character formatting inside it. Set paragraph-level properties (style, alignment, spacing, indent) on the `p`; set font / size / color / bold on the `r`. -# Table structure -officecli get doc.docx "/body/tbl[1]" --depth 3 +```bash +officecli add "$FILE" /body --type paragraph --prop text="Executive Summary" --prop style=Heading1 --prop size=18pt --prop bold=true --prop spaceAfter=12pt +officecli set "$FILE" "/body/p[1]/r[1]" --prop color=1F4E79 +``` -# Style definitions -officecli get doc.docx /styles +**Use styles, not ad-hoc formatting.** `style=Heading1` references the document's style definition — change the definition once, all headings update. Inline `size=18pt` on every heading is a style-bypass; when you need to retheme you have to touch every paragraph. -# Specific style -officecli get doc.docx "/styles/Heading1" +Use `spaceBefore` / `spaceAfter` for vertical spacing. Never use chains of empty paragraphs — they break pagination and are flagged by `view issues`. -# Header/footer -officecli get doc.docx "/header[1]" -officecli get doc.docx "/footer[1]" +### Tables -# Numbering definitions -officecli get doc.docx /numbering +Tables are `/body/tbl[N]` with rows `tr[N]` and cells `tc[N]`. Add the table with a row and column count, then fill. -# JSON output for scripting -officecli get doc.docx "/body/p[1]" --json +```bash +officecli add "$FILE" /body --type table --prop rows=4 --prop cols=3 --prop width=100% +officecli set "$FILE" "/body/tbl[1]/tr[1]" --prop header=true --prop c1=Quarter --prop c2="Revenue" --prop c3="Growth" +officecli set "$FILE" "/body/tbl[1]/tr[1]/tc[1]/p[1]/r[1]" --prop bold=true ``` -### CSS-like Queries +Row-level `set` supports `height`, `header`, and `c1 / c2 / ... / cN` text shortcuts — `cN` generalises to any column count, use as many as the table has columns (a 7-column matrix accepts `c1` through `c7`). Cell formatting (bold, fill, color) goes on the cell's paragraph / run. For per-cell borders, use the paragraph-level `pbdr.*` dotted-attr on the cell's inner paragraph instead of cell-level `border.bottom` (the cell-level border prop currently places `` in the wrong XML position and fails `validate` — see Known Issues). + +### Lists (bullets, numbered, multi-level) + +For single-level bullets or numbers, set `listStyle` on the paragraph (`listStyle` is a paragraph prop, NOT a run prop — common mistake): ```bash -# Find paragraphs by style -officecli query doc.docx 'paragraph[style=Heading1]' +officecli add "$FILE" /body --type paragraph --prop text="First item" --prop listStyle=bullet +officecli add "$FILE" /body --type paragraph --prop text="Second item" --prop listStyle=bullet +``` -# Find paragraphs containing text -officecli query doc.docx 'p:contains("quarterly")' +For multi-level (legal-style 1 / 1.1 / 1.1.1 / appendix numbering), add an `abstractNum` then a `num`, then reference the `numId` from each paragraph: -# Find empty paragraphs -officecli query doc.docx 'p:empty' +```bash +officecli add "$FILE" /numbering --type abstractnum --prop format=decimal +officecli add "$FILE" /numbering --type num --prop abstractNumId=1 +officecli add "$FILE" /body --type paragraph --prop text="Section one" --prop numId=1 --prop ilvl=0 +``` -# Find images without alt text -officecli query doc.docx 'image:no-alt' +After adding, verify with `officecli query "$FILE" 'paragraph[numId>0]'` that every `numId` reference points at a real ``. See `officecli help docx abstractnum` and `officecli help docx num` for all level and format options. -# Find bold runs in centered paragraphs -officecli query doc.docx 'p[alignment=center] > r[bold=true]' +### Tab stops (dot leaders, right-aligned page numbers) -# Find large text -officecli query doc.docx 'paragraph[size>=24pt]' +Used for positional layout — a signature line, a TOC-entry-style "Chapter 1 ........ 12" row, a form field slot. Tab stops are a first-class `tab` element added as a child of the paragraph: -# Find fields by type -officecli query doc.docx 'field[fieldType!=page]' +```bash +officecli add "$FILE" "/body/p[1]" --type tab --prop pos=6in --prop val=right --prop leader=dot +officecli add "$FILE" "/body/p[2]" --type tab --prop pos=3cm --prop val=left --prop leader=underscore ``` ---- -# officecli: v1.0.23 +`pos` accepts `6in` / `6cm` / twips. `val` ∈ `left` / `center` / `right`. `leader` ∈ `none` / `dot` / `hyphen` / `underscore`. Paths are 1-based: `/body/p[N]/tab[K]`. See `officecli help docx tab` for the full grammar. -## Design Principles +**Leader rendering caveat.** `leader=dot` / `underscore` on a tab definition alone does not emit dots/underscore in the output — the leader only renders when a real `` character is present inside a run of that paragraph, and the high-level API does not insert `` runs. For visible signature lines or dot-leader TOC-style rows you have two working options: (a) use literal characters — `text="_______________________________________"` for a signature line, or `"Chapter 1 ............ 12"` for a leader row — visually equivalent and ships reliably; or (b) `raw-set` a `` into the paragraph before the leading line. -**Professional documents need clear structure and consistent formatting.** +### Fields (PAGE / NUMPAGES / DATE / MERGEFIELD / REF) -### Document Structure +Fields are live values computed at render time. Two props carry all the info: `fieldType` picks the field; `name` supplies the target (merge field name or bookmark for `ref`); `format` adds switches (date patterns, number formats). -Every document needs clear hierarchy -- title, headings, subheadings, body text. Don't create a wall of unstyled Normal paragraphs. +| Field | Use | Example | +|---|---|---| +| `page` | current page number | `--prop field=page` on footer, or `--prop fieldType=page` inline | +| `numpages` | total pages | `--prop field=numpages` / `--prop fieldType=numpages` | +| `date` | today | `--prop fieldType=date --prop format='yyyy-MM-dd'` | +| `mergefield` | template merge token | `--prop fieldType=mergefield --prop name=CustomerName` | +| `ref` | cross-reference to a bookmark | `--prop fieldType=ref --prop name=bookmarkName` | -### Typography +The full `fieldType` enum (30+ values: `page`, `pagenum`, `pagenumber`, `numpages`, `date`, `time`, `author`, `title`, `filename`, `section`, `sectionpages`, `mergefield`, `ref`, `pageref`, `noteref`, `seq`, `styleref`, `docproperty`, `if`, `createdate`, `savedate`, `printdate`, `edittime`, `lastsavedby`, `subject`, `numwords`, `numchars`, `revnum`, `template`, `comments`, `keywords`) is in `officecli help docx field`. **There is NO `fieldInstr` fieldType** — use the `instr` prop (alias `instruction`) to inject raw field instruction text when typed shortcuts fall short. Picture switches (`MERGEFIELD Amount \# "#,##0.00"`, `DATE \@ "yyyy年MM月"`) go via `--prop instr='...'` on mergefield and via `--prop format='yyyy-MM-dd'` on date/time (mergefield's `format` prop is ignored with a warning — use `instr` instead). -Choose a readable body font (Calibri, Cambria, Georgia, Times New Roman). Keep body at 11-12pt. Headings should step up: **H1=18pt minimum (20pt preferred for long documents)**, H2=14pt bold, H3=12pt bold. +**SEQ / PAGEREF cached-value trap.** `seq` and `pageref` are CLI-expressible (`--prop fieldType=seq --prop identifier=Figure`, `--prop fieldType=pageref --prop name=bookmark`) and pass `validate`, but every instance emits cached `` of `1` regardless of position — so three `SEQ Figure` captions render as `Figure 1 / Figure 1 / Figure 1` in viewers that do not recompute on open. Set `` in settings (via `raw-set`) and/or patch the cached `` after each SEQ. Academic papers with multiple figures/tables: see the `officecli-academic-paper` skill for the full SEQ patch recipe. -### Spacing +For a standalone MERGEFIELD inside a paragraph: -Use paragraph spacing (`spaceBefore`/`spaceAfter`) instead of empty paragraphs. Line spacing of 1.15x-1.5x for body text. +```bash +officecli add "$FILE" "/body/p[3]" --type field --prop fieldType=mergefield --prop name=customer_name +# Renders as «customer_name» — visible placeholder, replaced in Word at mail-merge time. +``` + +Verified: canonical form passes `validate` and renders `«customer_name»` on open. Confirm all MERGEFIELDs exist with `officecli query "$FILE" 'field[fieldType=mergefield]'`. + +**MERGEFIELD templates: do NOT render placeholder literals.** If a template shows `{{customer_name}}` or `$NAME$` as body text, a human recipient sees the literal token — that is a failed template. Either (a) insert a real MERGEFIELD via the `field` type above, which Word replaces at mail-merge time, or (b) put literal tokens only inside an obvious instruction paragraph ("Replace `{{customer_name}}` before sending"). See Requirements for Outputs → Visual delivery floor. + +### Headers & Footers (page numbering) -### Page Setup +The single-command pattern — the CLI injects `` so you do not compose the field by hand: + +```bash +# Empty first-page footer — auto-enables differentFirstPage so the cover has no page number +officecli add "$FILE" / --type footer --prop type=first --prop text="" + +# Default footer with live page number +officecli add "$FILE" / --type footer --prop type=default --prop align=center --prop size=9pt --prop text="Page " --prop field=page +``` -Always set margins explicitly. US Letter default: `pageWidth=12240`, `pageHeight=15840`, margins=1440 (1 inch). +When both a first-page footer and a default footer exist, the default footer is `/footer[2]`. If only a default footer, it is `/footer[1]`. **Verify**: `get --depth 3` must show `fldChar` children, not just a run with literal text `"Page"`. `view outline` prints "Footer: Page" for both live fields AND static text — do not rely on it. -### Headers & Footers +Do NOT `set / --prop differentFirstPage=true` separately — that prop is UNSUPPORTED and silently fails. Adding a first-type footer is how you flip the bit. -Professional documents include page numbers at minimum. Consider company name in header, page X of Y in footer. +For composite footers like "Page X of Y" (PAGE + NUMPAGES in one paragraph), see `officecli help docx footer` and use `raw-set` with two `` field instructions — high-level single-command does not compose two fields in one run. -**Standard footer setup (always use this pattern for documents with a cover page):** +### Table of Contents -> **⚠️ Known CLI bug:** `--prop field=page` is **silently ignored** in `add --type footer` commands. The footer is created with static text only. You **must** use `raw-set` to inject the PAGE field after creating the footer. See the 3-step pattern below. +For any document with 3+ headings (Requirements): ```bash -# Adding type=first footer automatically enables differentFirstPage — no separate set command needed +officecli add "$FILE" /body --type toc --prop levels="1-3" --prop title="Table of Contents" --prop hyperlinks=true --index 0 +``` -# Step 1. Empty footer for cover page (CLI auto-inserts — suppresses cover page number) -officecli add doc.docx / --type footer --prop type=first --prop text="" +The TOC is a live field — when a human opens the file, the viewer either populates it on open or shows it after the user recalculates (F9 in Word). Do NOT pass `--prop pagenumbers=true` — UNSUPPORTED; page numbers render automatically. -# Step 2. Default footer with static "Page " text (field=page is silently ignored — do NOT rely on it) -officecli add doc.docx / --type footer --prop text="Page " --prop type=default --prop alignment=center --prop size=9pt --prop font=Calibri +**Addressing the TOC (1.0.60+).** Direct paths `/toc[1]` or `/tableofcontents` resolve to the first TOC field without hand-walking XPath — use these as the primary path for `get` / `set` / `remove`: -# Step 3. Inject PAGE field via raw-set (footer[2] = default when first-page footer also exists) -officecli raw-set doc.docx "/footer[2]" \ - --xpath "//w:p" \ - --action append \ - --xml ' PAGE ' +```bash +officecli get "$FILE" "/toc[1]" --depth 2 # primary path — no raw-set needed to locate +officecli get "$FILE" "/tableofcontents" --depth 2 # alias, same target ``` -> **Footer index rule:** When both a first-page footer and a default footer are added, the default footer is `/footer[2]`. If there is no first-page footer, the default footer is `/footer[1]`. Always verify with `officecli get doc.docx "/footer[2]"` (or `"/footer[1]"`) to confirm the `` element is present. +**TOC delivery step — treat this as mandatory before handing the file off.** **The live TOC field is a placeholder until recalculated.** Some viewers show the real heading list on first open; others show the literal string `Update field to see table of contents` until the reader recalculates. Two workarounds — pick one based on who reads the file: -> **LibreOffice rendering note:** Page number fields may display as static "Page" (not "Page 1") in LibreOffice PDF preview — this is a LibreOffice limitation. Open in Microsoft Word to see actual page numbers. Confirm the field is present with `officecli get doc.docx "/footer[2]"` — the output must show `fldChar` children, not just a plain run. +- **Recipients who will open in a viewer that recalculates (or who will press F9)**: add a visible instruction ("Press F9 to refresh the TOC and page numbers"). No further action needed. +- **Recipients who cannot / will not recalculate**: use the **static TOC fallback — see Report-level recipes (f) below**. No CLI-only pipeline currently populates `` with the cached heading rows that Word writes on save. Headless conversion tools cannot pre-render the TOC on Word's behalf — their TOC handling and pagination differ, so relying on them to "fill" the TOC for a Word recipient is unsafe. `raw-set` on `//w:sdt/w:sdtContent` is theoretically possible but requires reconstructing the exact per-heading XML (with correct bookmarks, PAGEREF chains, and cached page numbers) and has not worked reliably. Hand-write the static fallback instead. -### Table Design +Ship-check: `officecli query "$FILE" 'p:contains("Update field to see")'` must return empty whenever the reader won't recalculate. If it matches, the TOC is unpopulated — switch to recipe (f). -Alternate row shading for readability. Header row with contrasting background. Consistent cell padding. +### Images -### Color Usage +Pictures go inside a run. Alt text is mandatory for accessibility, but **add rejects `alt` at create time** (CLI bug C-D-3): add first, then `set`. -Use color sparingly in documents -- accent color for headings or table headers, not rainbow formatting. +```bash +officecli add "$FILE" "/body/p[5]" --type picture --prop src=chart.png --prop width=4in +officecli set "$FILE" "/body/p[5]/r[last()]" --prop alt="Q4 revenue by region, bar chart" +``` -### Content-to-Element Mapping +Confirm with `officecli query "$FILE" 'image:no-alt'` — output should be empty before delivery. -| Content Type | Recommended Element(s) | Why | -|---|---|---| -| Sequential items | Bulleted list (`listStyle=bullet`) | Scanning is faster than inline commas | -| Step-by-step process | Numbered list (`listStyle=numbered`) | Numbers communicate order | -| Comparative data | Table with header row | Columns enable side-by-side comparison | -| Trend data | Embedded chart (`chartType=line/column`) | Visual pattern recognition | -| Key definition | Hanging indent paragraph | Offset term from definition | -| Legal/contract clause | Numbered list with bookmarks | Cross-referencing via bookmarks | -| Mathematical content | Equation element (`formula=LaTeX`) | Proper OMML rendering | -| Citation/reference | Footnote or endnote | Keeps body text clean | -| Pull quote / callout | Paragraph with border + shading | Visual distinction from body | -| Multi-section layout | Section breaks with columns | Column control per section | +### Hyperlinks and bookmarks ---- -# officecli: v1.0.23 +External links go via `hyperlink`: -## QA (Required) +```bash +officecli add "$FILE" "/body/p[2]" --type hyperlink --prop uri="https://example.com" --prop text="our site" +``` -**Assume there are problems. Your job is to find them.** +**Internal links (to a bookmark within the document) are NOT supported by the high-level `hyperlink` command** — it rejects fragment URLs. Use `raw-set` with ``, or pair a `PAGEREF` field with visible text. See `officecli help docx hyperlink` and `officecli help docx bookmark`. + +### Sections and page setup + +Document root `/` carries page setup (`pageWidth`, `pageHeight`, margins). Multi-section documents (landscape insert, column layout) add a `section` break; use `officecli help docx section` for the section prop list. + +```bash +officecli set "$FILE" / --prop pageWidth=12240 --prop pageHeight=15840 --prop marginTop=1440 --prop marginLeft=1440 +``` + +Section accepts both camelCase (`pageWidth`, canonical) and lowercase alias (`pagewidth`). Prefer camelCase. -### Issue Detection +### Report-level recipes + +Four patterns that come up on every long-form report and aren't covered by the Quick Start. Each has been executed and `validate`-passed. + +**(a) Rich cover page — hit the ≥ 60% filled floor.** A bare title + date cover reads as unfinished. Stack a confidentiality banner, title, subtitle, client/project/date block, and a 3-line key-themes strip: ```bash -# Check for formatting, content, and structure issues automatically -officecli view doc.docx issues +officecli add "$FILE" /body --type paragraph --prop text="CONFIDENTIAL — CLIENT USE ONLY" --prop align=center --prop size=9pt --prop color=C00000 --prop spaceAfter=24pt +officecli add "$FILE" /body --type paragraph --prop text="Strategic Growth Review" --prop style=Title --prop size=32pt --prop bold=true --prop align=center --prop font=Cambria --prop spaceAfter=8pt +officecli add "$FILE" /body --type paragraph --prop text="FY26 Outlook and Scenario Planning" --prop italic=true --prop size=16pt --prop align=center --prop spaceAfter=36pt +officecli add "$FILE" /body --type paragraph --prop text='Prepared for: Acme Corp. Leadership Team' --prop align=center --prop size=11pt +officecli add "$FILE" /body --type paragraph --prop text='Engagement: 2026-04 — 2026-06' --prop align=center --prop size=11pt +officecli add "$FILE" /body --type paragraph --prop text='Author: Advisory Partners' --prop align=center --prop size=11pt --prop spaceAfter=36pt +officecli add "$FILE" /body --type paragraph --prop text="Key themes: 1) margin resilience, 2) EMEA expansion, 3) capital allocation." --prop align=center --prop italic=true --prop size=10pt +# Force the next section to start on a new page — belt-and-suspenders for cross-viewer reliability +# (pageBreakBefore alone is unreliable across viewers; --type pagebreak alone also flakes) +officecli add "$FILE" /body --type pagebreak +officecli set "$FILE" "/body/p[last()]" --prop pageBreakBefore=true +``` -# Filter by issue type -officecli view doc.docx issues --type format -officecli view doc.docx issues --type content -officecli view doc.docx issues --type structure +**(b) Page X of Y footer — composite PAGE + NUMPAGES.** Add the footer paragraph first, then three child ops build `Page of ` in one paragraph. Visual outcome: footer reads `Page 3 of 12` with both numbers live. This is the official `officecli help docx footer` recipe. + +```bash +officecli add "$FILE" / --type footer --prop type=default --prop text="Page " --prop align=center --prop size=9pt +officecli add "$FILE" "/footer[1]/p[1]" --type field --prop fieldType=page +officecli add "$FILE" "/footer[1]/p[1]" --type run --prop text=" of " +officecli add "$FILE" "/footer[1]/p[1]" --type field --prop fieldType=numpages +# Verify the 3 field fragments exist: +officecli get "$FILE" "/footer[1]/p[1]" --depth 1 | grep -o fldChar | wc -l # expect ≥ 4 (begin+separate+end per field; DON'T use `grep -c` — single-line XML always returns 1) ``` -### Content QA +**(c) Header row with fill and white bold text.** Don't chain `shd.fill=` (broken). Order matters: populate the header row's cell text FIRST (runs don't exist in empty cells, so a `set .../tc[N]/p[1]/r[1]` on empty cells errors with "No r found"), THEN apply cell fill, THEN run formatting. Visual outcome: dark-blue header band with white bold labels, zebra-striped data rows. ```bash -# Extract all text, check for missing content, typos, wrong order -officecli view doc.docx text +officecli add "$FILE" /body --type table --prop rows=5 --prop cols=4 --prop width=100% +# 1. Populate header cell text — creates the runs we'll style next +officecli set "$FILE" "/body/tbl[1]/tr[1]" --prop header=true --prop c1=Quarter --prop c2=Revenue --prop c3=Growth --prop c4=Status +# 2. Header cells — dark fill + white bold text +for col in 1 2 3 4; do + officecli set "$FILE" "/body/tbl[1]/tr[1]/tc[$col]" --prop fill=1F4E79 + officecli set "$FILE" "/body/tbl[1]/tr[1]/tc[$col]/p[1]/r[1]" --prop bold=true --prop color=FFFFFF +done +# 3. Alternating row fills for rows 3, 5 (zebra) +for row in 3 5; do for col in 1 2 3 4; do + officecli set "$FILE" "/body/tbl[1]/tr[$row]/tc[$col]" --prop fill=D9E2F3 +done; done +``` -# Check structure -officecli view doc.docx outline +Verified: without step 1, step 2's run-level `set` errors because empty cells have no `r`. This is the most common trip in table builds. -# Check for empty paragraphs (common clutter) -officecli query doc.docx 'p:empty' +**(d) Financial table style — right-align numbers, bold totals, bottom border on total row.** Numbers read right-aligned; totals read bold; a `pbdr.bottom` under the last data row visually separates the total: -# Check for images without alt text -officecli query doc.docx 'image:no-alt' +```bash +# Right-align number columns (cols 2-4), paragraph-level +for row in 2 3 4 5; do for col in 2 3 4; do + officecli set "$FILE" "/body/tbl[1]/tr[$row]/tc[$col]/p[1]" --prop align=right +done; done +# Total row (row 5) bold + bottom border on the data paragraphs +for col in 1 2 3 4; do + officecli set "$FILE" "/body/tbl[1]/tr[5]/tc[$col]/p[1]/r[1]" --prop bold=true + officecli set "$FILE" "/body/tbl[1]/tr[4]/tc[$col]/p[1]" --prop pbdr.bottom="single;6;000000;0" +done ``` -When editing templates, check for leftover placeholder text: +**(e) Cell with multiple bullets — SWOT / risk matrix / timeline.** Row-level `c1="line1\nline2"` drops a literal `\n`; one cell = one paragraph by default. To stack N bullets inside a single cell, seed the first via `set c1=`, then `add paragraph` under the cell for each subsequent bullet, then `move --index 1` to push the seeded line above its siblings if needed. Visual outcome: a 2×2 SWOT where each quadrant lists 3-5 bullets, each on its own line. ```bash -officecli query doc.docx 'p:contains("lorem")' -officecli query doc.docx 'p:contains("xxxx")' -officecli query doc.docx 'p:contains("placeholder")' +# 2x2 SWOT, cell (1,1) = Strengths with 3 bullets +officecli set "$FILE" "/body/tbl[1]/tr[1]" --prop c1="Installed base of 18k enterprise seats" +officecli add "$FILE" "/body/tbl[1]/tr[1]/tc[1]" --type paragraph --prop text="Margin structure above peer median" --prop listStyle=bullet +officecli add "$FILE" "/body/tbl[1]/tr[1]/tc[1]" --type paragraph --prop text="Founder-led sales motion in mid-market" --prop listStyle=bullet +# (optional) If the seeded line should also render as a bullet, style it: +officecli set "$FILE" "/body/tbl[1]/tr[1]/tc[1]/p[1]" --prop listStyle=bullet ``` -### Validation +If your seed paragraph lands at the bottom instead of the top (row-level `set c1=` sometimes appends), re-order: `officecli move "$FILE" "/body/tbl[1]/tr[1]/tc[1]/p[N]" --index 0`. + +**(f) Static TOC fallback (cross-viewer reliability).** When delivering to viewers that don't auto-recalculate fields, the live TOC field renders as the literal `Update field to see table of contents`. No CLI-only pipeline can pre-populate a TOC field the way Word does on save — this is a hard black hole, not a recipe gap. Workaround: remove the TOC field, keep the `TOCHeading` style paragraph as a visible header, then hand-write one paragraph per heading with a literal dot-leader line. Visual outcome: a plain text TOC with dots trailing to page numbers, no live field, ships correctly in any reader. ```bash -officecli validate doc.docx +# 1. Locate and remove the raw TOC field paragraph(s) that carry the "Update field to see..." cached text +officecli query "$FILE" 'p:contains("Update field to see")' # note the /body/p[N] paths +officecli remove "$FILE" "/body/p[N]" # repeat per hit + +# 2. Add a visible heading where the TOC used to be (if not already present) +officecli add "$FILE" /body --type paragraph --prop text="Contents" --prop style=TOCHeading --prop size=14pt --prop bold=true --index + +# 3. Hand-write one line per heading with literal dots and page number +officecli add "$FILE" /body --type paragraph --prop text="1. Executive Summary ......................................... 3" --prop size=11pt --index +officecli add "$FILE" /body --type paragraph --prop text="2. Market Diagnosis .......................................... 5" --prop size=11pt --index +# ... one per heading ``` -### Pre-Delivery Checklist +Use this when the live-field option leaves the literal prompt visible to the reader. Page numbers are manually set. For approximate pagination preview: `officecli view "$FILE" html` and read the returned HTML file to eyeball layout. For exact page numbers: open in your target viewer (Word / WPS / etc.) — precise numbers only come from the final render in that viewer. This recipe assumes you can get approximate page positions from the document structure. `add --type toc` (live field) remains correct for recipients whose viewer recalculates on open (or who will press F9) — this recipe is for everyone else. -- [ ] Metadata set (title, author) -- [ ] 页码字段已注入 — 用 `officecli get doc.docx "/footer[2]" --depth 3` 确认输出中有 `fldChar` 元素。 - ⚠️ `view outline` 显示 "Footer: Page" 时**无法**区分静态文字和动态字段 — 必须用 `get --depth 3` 命令验证 `fldChar` 存在。 - 如无 first-page footer,用 `"/footer[1]"` 替代 `"/footer[2]"`。**Required: raw-set PAGE field injection** — `--prop field=page` in add command is silently ignored. -- [ ] First-page footer added (`--type footer --prop type=first --prop text=""`) if document has a cover page — CLI automatically enables differentFirstPage (no separate set command needed) -- [ ] Cover page content fills >= 60% of the page (has accent bars, subtitle, author, date, contact info) — **lower half must not be >40% empty**: if the title block ends above the page midpoint, add an abstract excerpt block, document scope statement, key highlights list, or decorative closing band. See creating.md → Cover Page Design for templates. -- [ ] **[REQUIRED]** TOC present when document has 3 or more headings — add with `officecli add doc.docx /body --type toc --prop levels="1-3" --prop title="Table of Contents" --prop hyperlinks=true --prop pagenumbers=true --index 0`; TOC displays as a field code in CLI output — press F9 in Word to refresh/render it -- [ ] Last page content fills >= 40% of the page (has conclusion / next steps / contact info / legal notice) -- [ ] Heading hierarchy is correct (no skipped levels, e.g., H1 -> H3) -- [ ] No empty paragraphs used as spacing (use spaceBefore/spaceAfter instead) -- [ ] All images have alt text -- [ ] Tables have header rows -- [ ] Document validates with `officecli validate` -- [ ] No placeholder text remaining +### Forcing page breaks — belt-and-suspenders for cross-viewer reliability -### Verification Loop +Two mechanisms exist; **neither alone is reliable across every viewer**. Pagination is heuristic — depending on the viewer and preceding content state, it may silently ignore `` OR render `` as a soft break. The two failures occur in opposite directions depending on the viewer. Apply BOTH on every H1 you want on a fresh page: -1. Generate document -2. Run `view issues` + `view outline` + `view text` + `validate` -3. List issues found (if none found, look again more critically) -4. Fix issues -5. Re-verify -- one fix often creates another problem -6. Repeat until a full pass reveals no new issues +```bash +# 1. Prepend a pagebreak element BEFORE the heading +officecli add "$FILE" /body --type pagebreak --index +# 2. Set pageBreakBefore=true on the heading paragraph itself +officecli set "$FILE" "/body/p[]" --prop pageBreakBefore=true +``` -**Do not declare success until you've completed at least one fix-and-verify cycle.** +Neither alone guarantees a break in every client. Observed on officecli 1.0.60: `pageBreakBefore` alone left 9 chapters mashed into 6 pages in one viewer; `--type pagebreak` alone has also been seen to flake, especially when the file is PDF-converted by a headless renderer. **Recommendation: prefer `pageBreakBefore=true` (more reliable across viewers) and add `--type pagebreak` as the secondary guarantee.** The redundant pair closes the gap. -**NOTE**: Unlike pptx, there is no visual preview mode (`view svg`/`view html`) for docx. Content verification relies on `view text`, `view annotated`, `view outline`, `view issues`, and `validate`. For visual verification, the user must open the file in Word. +**`break=newPage` alias (1.0.61+).** The paragraph / section prop `--prop break=newPage` is a shorter alias that maps to `pageBreakBefore=true` (accepts `newPage | page | nextPage | pageBreak`). Same underlying XML, same behavior — so the belt-and-suspenders rule still applies: use `add --type pagebreak` before the heading AND set `pageBreakBefore=true` / `break=newPage` on the heading paragraph itself. ⚠️ `pageBreakBefore`/`break=` passed to `add` may be silently dropped — always apply it via a subsequent `set`. -**QA display notes:** -- `view text` shows "1." for ALL numbered list items regardless of their actual rendered number. This is a display limitation -- the actual document renders correct auto-incrementing numbers (1, 2, 3...) in Word and LibreOffice. Do not treat this as a defect. -- `view issues` flags "body paragraph missing first-line indent" on any paragraph that lacks a first-line indent — this includes cover page paragraphs, centered headings, list items, bibliography entries, callout boxes, and any block-style paragraph with explicit `spaceAfter`. These warnings are expected and can be ignored. First-line indent is only required in APA/academic body text — most professional documents use block style (no first-line indent) by default. +Apply to every H1, the TOC heading, and the cover-closing paragraph. Preview via `view html` (read the returned HTML path) and count pages to confirm. ---- -# officecli: v1.0.23 - -## Common Pitfalls - -| Pitfall | Correct Approach | -|---------|-----------------| -| `--name "foo"` | Use `--prop name="foo"` -- all attributes go through `--prop` | -| Guessing property names | Run `officecli docx set paragraph` to see exact names | -| `\n` in shell strings | Use `\\n` for newlines in `--prop text="line1\\nline2"` | -| Modifying an open file | Close the file in Word first | -| Hex colors with `#` | Use `FF0000` not `#FF0000` -- no hash prefix | -| Paths are 1-based | `/body/p[1]`, `/body/tbl[1]` -- XPath convention | -| `--index` is 0-based | `--index 0` = first position -- array convention | -| Unquoted `[N]` in zsh/bash | Shell glob-expands `/body/p[1]` -- always quote paths: `"/body/p[1]"` | -| Spacing in raw numbers | Use unit-qualified values: `'12pt'`, `'0.5cm'`, `'1.5x'` not raw twips | -| Empty paragraphs for spacing | Use `spaceBefore`/`spaceAfter` properties on paragraphs | -| `$` in `--prop text=` (shell) | `--prop text="$50M"` strips the value. Use single quotes: `--prop text='$50M'` | -| `$` and `'` in batch JSON | Use heredoc: `cat <<'EOF' \| officecli batch` -- single-quoted delimiter prevents shell expansion | -| Wrong border format | Use `style;size;color;space` format: `single;4;FF0000;1` | -| listStyle on run instead of paragraph | `listStyle` is a paragraph property, not a run property | -| Row-level bold/color/shd | Row `set` only supports `height`, `header`, and `c1/c2/c3` text shortcuts. Use cell-level `set` for formatting (bold, shd, color, font) | -| Section vs root property names | Section uses `pagewidth`/`pageheight` (lowercase). Document root uses `pageWidth`/`pageHeight` (camelCase) | -| `--prop field=page` in footer | **SILENTLY IGNORED** in `add --type footer` commands. The footer is created with static text only. Must use `raw-set` to inject `` after creating the footer. See Headers & Footers section for the 3-step pattern. | -| Page number on cover | Adding `--type footer --prop type=first` automatically enables differentFirstPage. Do NOT use `set / --prop differentFirstPage=true` — that prop is UNSUPPORTED and silently fails | -| TOC skipped for multi-heading docs | Any document with 3+ headings requires a TOC. It is not optional — add with `--type toc --index 0` after the cover page break | -| Code block indentation via spaces | Use the `ind.left` paragraph property (e.g. `--prop ind.left=720`) for code block indentation — consecutive spaces as padding produce `view issues` warnings and visually inconsistent results | +### Template delivery — separating Template Notes from end-user content ---- -# officecli: v1.0.23 +HR / legal / vendor templates commonly carry internal-only guidance ("replace `{{CompanyName}}`", "list of expected merge columns") that must NOT ship to the end recipient. Two working patterns: + +- **Trailing "Template Notes" section with a clear heading.** Add a `Heading 1` titled "Template Notes for HR Users" (or similar) at the bottom of the document, then all instruction paragraphs underneath. Before distribution, `officecli remove "$FILE" /body/p[N]` every paragraph from the heading downward, or `officecli query "$FILE" 'paragraph[style=Heading1]:contains("Template Notes")'` to locate the boundary. A visible heading makes the section unmistakable at review time and scriptable at delivery time. +- **Bookmark-bounded internal section.** Wrap the guidance between two bookmarks (`add --type bookmark --prop name=__template_notes_start` / `_end`) on the paragraphs before and after the internal content. At delivery, `raw-set` removes everything between the two anchors in one pass. Slightly more fragile but more robust to accidental heading edits. + +Either way, the ship-check is: after removal, `officecli query "$FILE" 'p:contains("Template Notes")'` returns empty AND `query 'p:contains("{{")` (literal tokens the guide referenced) also returns empty. If the template notes paragraph survives, a downstream employee will read internal HR language. Treat this as a delivery gate for template builds. + +### Advanced / specialty topics (skip if you are writing a report) -## Performance: Resident Mode +Reports, memos, letters, proposals, and HR templates don't need this section — skip to Raw-set escape hatch. Keep reading only if your document is academic (equations, footnotes, bibliography), a reviewed draft (comments, tracked changes), or marked (watermark). -**Always use `open`/`close` — it is the smart default, not a special-case optimization.** Every command benefits: no repeated file I/O. +**Equations and footnotes.** `--type equation` takes LaTeX — `\frac`, `\sum`, Greek letters, `\mathit` render; `\mathcal` emits invalid XML (use `\mathit` instead). Footnotes auto-number by paragraph index. ```bash -officecli open doc.docx # Load once into memory -officecli add doc.docx ... # All commands run in memory — fast -officecli set doc.docx ... -officecli close doc.docx # Write once to disk +officecli add "$FILE" /body --type equation --prop formula="\\frac{a}{b} + \\sum_{i=1}^{n} x_i" +officecli add "$FILE" "/body/p[3]" --type footnote --prop text="See Appendix A for methodology." ``` -Use this pattern for every document build, regardless of command count. +`--type equation` always creates a standalone `/body/oMathPara[N]` block — never an inline run, even if you pass a paragraph path. For inline math inside running text, `raw-set` an `` (not ``) as a run child. Bibliography with hanging indent: `firstLineIndent=-720 indent=720` per entry (dotted `ind.hanging` is not canonical — see Known Issues). -## Performance: Batch Mode +**docx vs academic-paper skill — when to switch.** Stay in docx for: chapter drafts, ≤ 3 footnotes, ≤ 2 equations, no bibliography, no cross-refs. Switch to `academic-paper` when you need ANY of: citation styles (APA / Chicago / Harvard / IEEE / GB 7714), in-text ↔ reference list auto-linking, numbered equations with `\ref`, "List of Figures", auto-updating "see Section 3.2" cross-refs, or author-year ↔ numeric style toggles. -Execute multiple operations in a single open/save cycle: +**docx vs word-form skill — when to switch.** Stay in docx for any report, letter, memo, or proposal. Switch to `officecli-word-form` when the document's purpose is **data capture** — fillable intake forms, contracts / SOWs with user-fill slots, HR onboarding forms, medical questionnaires, compliance checklists, mail-merge templates. Those carry `` content controls, `` legacy form fields, or `documentProtection=forms`, none of which this skill teaches. + +**Comments and tracked changes.** Bulk accept/reject: `set / --prop accept-changes=all` (or `reject-changes=all`). Locate individual changes with `query ins` and `query del` — NOT `query trackedchange` (CLI bug C-D-1). Adding an `` or `` from scratch requires `raw-set`. Add a comment with `add "/body/p[4]" --type comment --prop author=... --prop text=...`. Reply threading (`parentId`) and `done=true` resolution are UNSUPPORTED — see C-D-2 / C-D-5 for `raw-set` workarounds. + +**Watermark.** Two steps because `add --prop opacity=...` is UNSUPPORTED (C-D-7): `add / --type watermark --prop text="DRAFT" --prop color=BFBFBF`, then `set /watermark --prop opacity=0.8`. Default opacity is 0.5. + +### Raw-set escape hatch (L1 / L2 / L3) + +Three tiers of precision; use the lowest that does the job. + +- **L1 — high-level props** (`--prop text=...`, `--prop style=Heading1`): your default. Works for 80% of cases. +- **L2 — dotted-attr fallback** (`pbdr.top=`, `ind.left=`, `padding.top=`, `border.*`, `font.size=`, `font.color=`): when L1 lacks the exact knob. Schema-safe for most props. Example: `--prop pbdr.bottom="single;6;1F4E79;0"`. Prefer this over raw-set when the whitelist covers your need. **Two dotted props emit invalid XML today** — `shd.fill=` (missing `w:val`) and `ind.firstLine=` (placed after `w:jc` in `pPr`). Use the canonical L1 form of these instead: `shd=clear;FFFF00` and `firstLineIndent=360`. See Known Issues → Schema-invalid-on-emit. +- **L3 — `raw-set` with XML**: last resort. Tied to OOXML knowledge; no schema protection. Use for tracked-change creation, internal hyperlinks, composite PAGE+NUMPAGES, comment `parentId`, `commentsExtended` `done=1`. + +Borders go through the format `style;size;color;space`: `single;4;FF0000;1`. Hex colors never start with `#`: `FF0000`, not `#FF0000`. Scheme color names (`accent1..6`, `dark1`/`dark2`, `light1`/`light2`, `hyperlink`) are also accepted anywhere a hex color is (1.0.60+) — prefer hex when you need stable colors across themes. + +## QA (Required) + +**Assume there are problems. Your job is to find them.** + +Your first document is almost never correct. Treat QA as a bug hunt, not a confirmation step. If you found zero issues on first inspection, you were not looking hard enough. Headings look fine **until** you `view outline` and notice an H3 directly under an H1. The footer shows "Page 1" in `view text` **until** you `get --depth 3` and find it is a static run, not a field. + +### Minimum cycle before "done" + +1. `officecli view "$FILE" issues` — empty paras, missing alt text, formatting anomalies. +2. `officecli view "$FILE" outline` — heading hierarchy, TOC presence, section count. No skipped levels (H1 → H3). +3. `officecli view "$FILE" text --max-lines 400` — content pass: typos, stray `\$` / `\t` / `\n` literals, placeholder tokens. +4. Query for known classes of defect: + ```bash + officecli query "$FILE" 'p:contains("lorem")' + officecli query "$FILE" 'p:contains("xxxx")' + officecli query "$FILE" 'p:contains("TODO")' + officecli query "$FILE" 'p:contains("{{")' + officecli query "$FILE" 'p:empty' + officecli query "$FILE" 'image:no-alt' + ``` +5. `officecli validate "$FILE"` — schema check. Close any resident first (see Known Issues). +6. **Visual pass — walk every page via the HTML preview.** Run `officecli view "$FILE" html` and Read the returned HTML path. Walk every page. "validate pass" is not delivery; "the preview looks like a real document" is delivery. For human review, run `officecli watch "$FILE"` (user opens the live preview at their own discretion) or have them open the `.docx` directly in Word / WPS. +7. If anything failed, fix, then **rerun the full cycle**. One fix commonly creates another problem. + +### Delivery Gate (run before handing off — any failure = REJECT, do NOT deliver) + +Copy-paste this block, set `FILE`, and refuse to declare done until every gate prints its OK line. `REJECT` aborts with exit 1 — the file is NOT deliverable. ```bash -cat <<'EOF' | officecli batch doc.docx -[ - {"command":"add","parent":"/body","type":"paragraph","props":{"text":"Introduction","style":"Heading1"}}, - {"command":"add","parent":"/body","type":"paragraph","props":{"text":"This report covers Q4 results.","font":"Calibri","size":"11pt"}} -] -EOF +FILE="your-file.docx" + +# Gate 1 — schema. Any error = REJECT. +officecli close "$FILE" 2>/dev/null +officecli validate "$FILE" | grep -q "no errors found" || { echo "REJECT Gate 1: validate failed"; exit 1; } +echo "Gate 1 OK" + +# Gate 2 — token leak (shell-escape / template tokens / TOC placeholder / literal \$ \t \n). +# COUNT-then-if pattern: grep -c never false-PASSes. +LEAK=$(officecli view "$FILE" text | grep -cE '(\$[A-Za-z_]+\$|\{\{[^}]+\}\}||xxxx|lorem|Update field to see|\\[\$tn])') +[ "$LEAK" -eq 0 ] && echo "Gate 2 OK" || { echo "REJECT Gate 2: $LEAK token-leak line(s)"; officecli view "$FILE" text | grep -nE '(\$[A-Za-z_]+\$|\{\{[^}]+\}\}||xxxx|lorem|Update field to see|\\[\$tn])'; exit 1; } + +# Gate 3 — live PAGE field exists when a footer is expected. +FLD=$(officecli query "$FILE" 'field[fieldType=page]' --json | jq '.data.results | length') +[ "$FLD" -ge 1 ] && echo "Gate 3 OK" || { echo "REJECT Gate 3: no live PAGE field"; exit 1; } +echo "Delivery Gate PASS" ``` -Batch supports: `add`, `set`, `get`, `query`, `remove`, `move`, `view`, `raw`, `raw-set`, `validate`. +Every gate must print its OK line before you declare the file delivered. -Batch fields: `command`, `path`, `parent`, `type`, `from`, `to`, `index`, `props` (dict), `selector`, `mode`, `depth`, `part`, `xpath`, `action`, `xml`. +### Field / cached-value spot-check -`parent` = container to add into (for `add`). `path` = element to modify (for `set`, `get`, `remove`, `move`). +TOC, PAGE, NUMPAGES, MERGEFIELD are all fields with **cached values** that may be stale or empty at write time. Confirm existence by structure, not by text. ---- -# officecli: v1.0.23 +- [ ] Footer PAGE field: `get /footer[N] --depth 3` lists the runs that carry the `fldChar begin` / `instrText` / `fldChar separate` / cached value / `fldChar end` chain — expect ≥ 5 runs for a single PAGE, ≥ 11 for composite "Page X of Y". For the underlying `` XML, use `officecli raw "$FILE" "/footer[1]" | grep -o fldChar | wc -l` (NOT `grep -c` — single-line XML returns 1, false-PASS risk), or run `officecli query "$FILE" 'field[fieldType=page]'` for a semantic match. If you see a single run with text `"Page"`, the field is missing — re-add with `--prop field=page`. +- [ ] TOC: `get /body/toc[1] --depth 2` must show field structure. In some viewers the TOC shows `1 1 1 1` for page numbers or the literal `Update field to see table of contents` until recalculated (see TOC delivery step). +- [ ] MERGEFIELD: `query 'field[fieldType=mergefield]'` — one entry per template slot. No literal `{{name}}` text elsewhere. +- [ ] SEQ / PAGEREF (if your document uses them via raw-set): confirm each `` chain exists by `raw`-inspecting the `document.xml`. -## Known Issues +**Cross-viewer caveat on PAGE fields**: some viewers render PAGE field text as the literal word "Page" (no number) until the reader recalculates. This is a [RENDERER-BUG], not a skill defect. Judge by whether `fldChar` children exist, not by whether the visible text shows a digit. -| Issue | Workaround | -|---|---| -| **No visual preview** | Unlike pptx (SVG/HTML), docx has no built-in rendering. Use `view text`/`view outline`/`view annotated`/`view issues` for verification. Users must open in Word for visual check. | -| **Track changes creation requires raw XML** | OfficeCLI can accept/reject tracked changes (`set / --prop accept-changes=all`) but cannot create tracked changes (insertions/deletions with author markup) via high-level commands. Use `raw-set` with XML for tracked change creation. | -| **Tab stops may require raw XML** | Tab stop creation is not exposed in officecli docx high-level commands. Use `raw-set` to add tab stop definitions in paragraph properties. | -| **Chart series cannot be added after creation** | Same as pptx: `set --prop data=` can only update existing series, not add new ones. Delete and recreate the chart with all series in the `add` command. | -| **Complex numbering definitions** | `listStyle=bullet/numbered` covers simple cases. For multi-level lists with custom formatting, use `numId`/`numLevel` properties. Creating new numbering definitions may require understanding the numbering part. | -| **Shell quoting in batch with echo** | `echo '...' \| officecli batch` fails when JSON values contain apostrophes or `$`. Use heredoc: `cat <<'EOF' \| officecli batch doc.docx`. | -| **Batch intermittent failure** | Approximately 1-in-15 batch operations may fail with "Failed to send to resident" when using batch+resident mode. Retry the command, or close/reopen the file. Split large batch arrays into 10-15 operation chunks. | -| **Table-level `padding` produces invalid XML** | Do not use `set tbl[N] --prop padding=N`. It creates invalid `tblCellMar`. Use cell-level `padding.top`/`padding.bottom` instead. If already applied, remove with `raw-set --xpath "//w:tbl[N]/w:tblPr/w:tblCellMar" --action remove`. | -| **Internal hyperlinks not supported** | The `hyperlink` command only accepts absolute URIs (`https://...`). Fragment URLs (`#bookmark`) are rejected. For internal cross-references, use descriptive text or `raw-set` with ``. | -| **Table `--index` positioning unreliable** | `--index N` on `add /body --type table` may be ignored (table appends to end). `move` also may not work for tables. Workaround: add content in the desired order, or remove/re-add surrounding elements. | -| **`\mathcal` in equations causes validation errors** | The `\mathcal` LaTeX command generates invalid `m:scr` XML. Use `\mathit` or plain letters instead. | -| **`view text` shows "1." for all numbered items** | Display-only limitation. Rendered output in Word/LibreOffice shows correct auto-incrementing numbers. | -| **`chartType=pie`/`doughnut` in LibreOffice PDF** | **Do NOT use `chartType=pie` or `chartType=doughnut` when LibreOffice PDF delivery is required.** These chart types render without visible slices in LibreOffice PDF export — only labels and legend appear, slices are invisible. Use `chartType=column` or `chartType=bar` instead. Charts render correctly in Microsoft Word only. | +### Fresh eyes ---- -# officecli: v1.0.23 +When you finish a document, open it fresh. Read `view text` / HTML preview top-to-bottom as if you are a new reviewer — look for typos, formatting inconsistencies, missing headings, orphaned paragraphs, placeholder text that looks like content. + +### Honest limit + +`officecli validate` catches schema errors, not design errors. A document can pass `validate` with: +- wrong heading hierarchy (H1 → H3) +- wrong font sizes that "look like" Heading 1 but are literal 14pt on Normal +- placeholder tokens rendered as body text +- an empty first-page footer attached to a document that has no cover + +The checklist above — especially the HTML-preview visual pass and the field structure check — is how you catch what validation can't. + +### QA display notes (don't chase these) + +- `view text` shows `"1."` for every numbered list item regardless of rendered number. The actual rendered output increments correctly. Not a defect. +- `view issues` flags "body paragraph missing first-line indent" on cover-page paragraphs, centered headings, list items, bibliography entries, callout boxes. First-line indent is only required for APA/academic body text. On professional documents (block style) these warnings are expected. + +## Known Issues & Pitfalls + +Organized by source. When something "looks broken", attribute it before chasing it: + +- **[AGENT-ERROR]** — the document itself is wrong (structure / data / formatting). Fix the document. +- **[RENDERER-BUG]** — the document is correct; a specific viewer renders it differently. Don't chase. +- **[SKILL gap]** — the skill didn't teach the relevant rule. Open an issue against the skill. -## Help System +### Schema-invalid-on-emit — disabled APIs + working forms -**When unsure about property names, value formats, or command syntax, run help instead of guessing.** One help query is faster than guess-fail-retry loops. +These props exit 0 at write time but produce XML that fails `validate` on close. Use the working form on the right. + +| Disabled (causes schema error) | Working form | Where it hurts | +|---|---|---| +| `--prop shd.fill=XXXXXX` on paragraph | `--prop shd="clear;XXXXXX"` (canonical) — or for table cells, `--prop fill=XXXXXX` on the cell | `` emitted without required `w:val`; affects every paragraph-shaded row / cover band / callout | +| `--prop ind.firstLine=360` (dotted) | `--prop firstLineIndent=360` (canonical) | Dotted form emits `` AFTER `` in `pPr` — ordering violation. Breaks every indented body paragraph in APA-style academic writing | +| `--prop border.bottom=...` on a table cell (`tc`) | `--prop pbdr.bottom="single;6;1F4E79;0"` on the cell's inner paragraph | `` placed wrong inside ``. See C-D-4 | + +**Before shipping, confirm these props are not in your build pipeline**: ```bash -officecli docx set # All settable elements and their properties -officecli docx set paragraph # Paragraph properties in detail -officecli docx set table # Table properties -officecli docx add # All addable element types -officecli docx view # All view modes -officecli docx get # All navigable paths -officecli docx query # Query selector syntax +# In the command log / batch JSON, grep for the three failing forms +grep -nE '(shd\.fill|ind\.firstLine|border\.(top|bottom|left|right)[^a-z])' commands.log +# Any hit = rewrite the command with the working form on the right. ``` + +`raw-set` escape hatch if neither form fits: inject `` or reorder `` / `` after emit. Post-patching with a Python `zipfile` + XML edit is acceptable. + +### Shell escape — three layers to keep separate + +The CLI does not interpret `\$`, `\t`, `\n`. They land in your document as literal characters. + +1. **Shell level.** `$` in a prop value → single-quote the whole value: `--prop text='$50M'`. Unescaped `$50M` gets stripped to `M` by the shell. +2. **JSON level (batch).** Standard JSON escapes — `"\n"`, `"\t"`, `"\""`. A real newline inside a cell/paragraph goes via `"\n"` in JSON (CLI passes the real `\n` char to Word). Writing `\n` (two characters) in a shell-quoted `--prop text=` is a bug — Word shows `\n` text. +3. **Word level.** Word's own literal `\n` is not a newline — it is two characters. If you need a soft line break inside a run, use `` via `raw-set`, or split into separate paragraphs. + +If in doubt, `view text` after writing and compare character-for-character. + +### CLI bug backlog (short workarounds) + +Skill-layer workarounds; full CLI fixes pending. C-D-3 and C-D-4 are the two you will actually hit on a report build — the rest cluster around academic / reviewed-document territory (see Advanced / specialty topics). + +- **C-D-3 `add picture --prop alt=` silent drop.** Add the picture first, then `set` the `alt` on the resulting run — two commands. Confirm with `query 'image:no-alt'`. +- **C-D-4 cell-level `border.bottom` / per-side `border.*` schema error.** `` is placed in the wrong position inside `` and `validate` fails. Workaround: use paragraph-level `pbdr.*` on the cell's inner paragraph (`--prop pbdr.bottom="single;6;1F4E79;0"`), or fix structure with `raw-set`. + +Specialty-only (skip unless you hit them): + +- **C-D-1** `query trackedchange` returns empty → use `query ins` + `query del`. +- **C-D-2** `set /comments/comment[N] --prop done=true` silent no-op → `raw-set` into `commentsExtended.xml`. +- **C-D-5** Comment `--prop parentId=N` UNSUPPORTED → sibling comment, or `raw-set` ``. +- **C-D-6** `add num --prop abstractNumId=N` may silent-bind wrong when built-ins exist → `get /numbering --depth 2` after add, correct with `set /numbering/num[N] --prop abstractNumId=...`. +- **C-D-7** Watermark `opacity` asymmetric — `add` rejects, `set` accepts → two-step (see Advanced topics). + +### Renderer quirks (cross-viewer) + +`officecli view html` is the right tool for structural QA (overflow, placeholder leakage, hierarchy, layout) — Read the returned HTML path. Some features vary by the viewer the end user opens the file in. Observed divergences, all [RENDERER-BUG]: + +- **PAGE field may render as literal "Page" (no number)** in some viewers until the reader recalculates. Judge field presence by `get --depth 3` finding ``, not by eyeballing a digit. +- **TOC cached page numbers may read "1 1 1 1"** until a human opens the file and recalculates (F9 in Word). +- **Pie / doughnut chart fill may collapse to one color** in some viewers (column / bar render fine). Switch to column / bar or accept the render caveat. +- **Form-control checkboxes may render double-boxed** in some viewers. +- **OMML equation baselines** may shift across viewers; the underlying XML is identical. + +Before calling a color, field, or chart broken, open the file in the user's target viewer. If it looks correct there, it is a viewer quirk — do not chase. + +### `validate` caveats + +- **Do NOT run `validate` while a resident is open.** `view --open` and `validate` briefly conflict on the file; `validate` reports spurious `drawing` / `tableParts` errors. Always `officecli close ` first. +- **`validate` does not check design.** Heading hierarchy, typography, placeholder leakage, empty covers pass validate but fail delivery. See QA section. + +### Batch / resident mode + +- **Batch + resident occasional failure** (1-in-10 to 1-in-15). Symptom: "Failed to send to resident". Retry the command, or close/reopen the file. Split large batch arrays into ≤ 12-op chunks for reliability. +- **Echo into batch breaks on `$` / `'`.** Use heredoc: `cat <<'EOF' | officecli batch doc.docx` — single-quoted delimiter prevents shell expansion. +- **Table `--index` positioning unreliable.** `--index N` on table add may be ignored. Add content in the intended order; or remove/re-add surrounding elements. + +### Common pitfalls + +| Pitfall | Correct approach | +|---|---| +| `--index` vs `[N]` | `--index` is 0-based (array convention); `[N]` paths are 1-based (XPath) | +| Multiple `add --index N` with the same N | Each insert shifts later content down; reusing the same N puts subsequent items BEFORE earlier ones. Insert in reverse order, or use `move --after/--before` anchored on `paraId` | +| Unquoted `[N]` in zsh/bash | Quote every path: `"/body/p[1]"` | +| `[last]` as predicate | Must be `[last()]` with parens. `/body/tbl[last()]/tr[1]` valid; `[last]` throws "Malformed path segment" | +| Raw twips in spacing | Use unit-qualified values: `12pt`, `0.5cm`, `1.5x` | +| Empty paragraphs for spacing | Use `spaceBefore` / `spaceAfter` on paragraphs | +| Row-level `set` for formatting | Row `set` only supports `height`, `header`, `c1..cN` text. Format goes on cell paragraph / run | +| `listStyle` on a run | `listStyle` is a paragraph property | +| Indent via leading spaces | Use `--prop indent=720` (twips) for left indent, `--prop firstLineIndent=360` for first line, `--prop hangingIndent=720` for hanging. Leading spaces fire `view issues`. Dotted `ind.left` works; dotted `ind.firstLine` does NOT — use canonical names | +| Cover page number suppression via `set differentFirstPage=true` | UNSUPPORTED. Add a first-type footer instead: `--type footer --prop type=first --prop text=""` | +| TOC `--prop pagenumbers=true` | UNSUPPORTED. Page numbers render automatically | +| `--type pagebreak` OR `pageBreakBefore` alone not breaking across viewers | Apply BOTH: `add /body --type pagebreak` before the heading AND `set /body/p[N+1] --prop pageBreakBefore=true`. Some viewers heuristically drop either one; the pair is the only reliable recipe (see Forcing page breaks) | +| Row-level `c1="line1\nline2"` for multi-line cell | `\n` lands as a literal. Use recipe (e): seed one bullet, then `add paragraph` to the cell for each subsequent line | +| Raw-set when dotted-attr would work | Prefer L2 (`pbdr.top=`, `ind.left=`, `font.size=`) over L3 raw-set. `shd.fill=` and `ind.firstLine=` are NOT safe — use canonical `shd=clear;XXXXXX` and `firstLineIndent=N` | +| Next paragraph picks up the previous Heading style | If a Heading2 `Next body line` sneaks through, set explicit `--prop style=Normal` on the following paragraph | +| Modifying a file open in Word | Close it in Word first | + +### Help pointer + +When in doubt: `officecli help docx`, `officecli help docx `, `officecli help docx `, `--json` for agents. Help is the authoritative schema; this skill is the decision guide. diff --git a/skills/officecli-docx/creating.md b/skills/officecli-docx/creating.md deleted file mode 100644 index eb73e36d5..000000000 --- a/skills/officecli-docx/creating.md +++ /dev/null @@ -1,1223 +0,0 @@ - - -# Creating Documents from Scratch - -Use this guide when creating a new document with no template. - -## Workflow Overview - -1. **Create** blank document -2. **Plan** document structure (outline + element types) -3. **Build** incrementally — run each command and check output before proceeding; use `batch` only for bulk content entry (many paragraphs or table cells at once) -4. **QA** (content + validation) -- see [SKILL.md](SKILL.md#qa-required) - ---- - -## Setup - -```bash -# Create blank document -officecli create doc.docx - -# Set metadata -officecli set doc.docx / --prop title="Q4 Report" --prop author="Team Alpha" - -# Set page size (US Letter with 1" margins) -officecli set doc.docx / --prop pageWidth=12240 --prop pageHeight=15840 --prop marginTop=1440 --prop marginBottom=1440 --prop marginLeft=1440 --prop marginRight=1440 - -# Set default font -officecli set doc.docx / --prop defaultFont=Calibri -``` - -### Page Size Reference - -| Paper | pageWidth | pageHeight | -|-------|-----------|------------| -| US Letter | 12240 | 15840 | -| A4 | 11906 | 16838 | -| Legal | 12240 | 20160 | - -Values are in twips (1440 twips = 1 inch, 567 twips = 1 cm). - ---- - -## Execution Strategy: Batch vs Incremental - -**Use INCREMENTAL (one command at a time):** -- `add /styles --type style` — define all styles before using them; verify they exist -- `add / --type header/footer/watermark/toc` — structural; verify before building on top -- `add /body --type table/chart` — creates the container; fill contents after confirming -- `validate` — always run alone -- **When in doubt** — a single command gives immediate feedback; if it fails you know exactly where. Batch errors are harder to diagnose. - -**Use BATCH (heredoc):** -- Multiple consecutive `add /body --type paragraph/run` — body content has no structural side effects -- Bulk list items (bullet points, numbered steps) -- Format painting — applying the same props to multiple paragraphs or table cells -- Filling table rows with text - -**Always use `officecli open`/`close`.** It keeps the file in memory so every command skips repeated file I/O — this is the smart default, not a special optimization. Batch and resident mode are independent: each works on its own, and they can be combined. - -```bash -# Batch: add multiple paragraphs at once -cat <<'EOF' | officecli batch report.docx -[ - {"command":"add","parent":"/body","type":"paragraph","props":{"text":"Q4 Business Report","alignment":"center","size":"28pt","bold":true,"color":"1F4E79"}}, - {"command":"add","parent":"/body","type":"paragraph","props":{"text":"Fiscal Year 2025","alignment":"center","size":"14pt","color":"4472C4"}}, - {"command":"add","parent":"/body","type":"paragraph","props":{"text":"Prepared by: Team Alpha","alignment":"center","color":"666666"}} -] -EOF - -# Batch: format painting — same props on multiple table header cells -cat <<'EOF' | officecli batch report.docx -[ - {"command":"set","path":"/body/tbl[1]/tr[1]/tc[1]","props":{"bold":true,"shd":"1F4E79","color":"FFFFFF"}}, - {"command":"set","path":"/body/tbl[1]/tr[1]/tc[2]","props":{"bold":true,"shd":"1F4E79","color":"FFFFFF"}}, - {"command":"set","path":"/body/tbl[1]/tr[1]/tc[3]","props":{"bold":true,"shd":"1F4E79","color":"FFFFFF"}} -] -EOF -``` - -**Batch chunk size:** Keep batches under 15 operations. Split by section (e.g., one batch per heading + its body paragraphs). - ---- - -## Document Structure Recipes - -Complete recipes for common document types. Each recipe shows the full command sequence for reference. - -> **Execute recipes incrementally — one command (or one `batch` block) at a time, not as a single shell script.** Read the output after each command. If a command fails, fix it before continuing. After each structural phase (styles, headers/footers, tables, charts), verify with `validate` or `get` before proceeding. - -### Recipe: Business Report - -```bash -# Create and open (resident mode for many operations) -officecli create report.docx -officecli open report.docx - -# Metadata and page setup -officecli set report.docx / --prop title="Q4 Business Report" --prop author="Team Alpha" -officecli set report.docx / --prop pageWidth=12240 --prop pageHeight=15840 --prop marginTop=1440 --prop marginBottom=1440 --prop marginLeft=1440 --prop marginRight=1440 -officecli set report.docx / --prop defaultFont=Calibri - -# Define heading styles (blank documents have no built-in style formatting) -officecli add report.docx /styles --type style --prop name="Heading 1" --prop id=Heading1 --prop type=paragraph --prop font=Calibri --prop size=20pt --prop bold=true --prop color=1F4E79 --prop spaceBefore=24pt --prop spaceAfter=12pt --prop keepNext=true -officecli add report.docx /styles --type style --prop name="Heading 2" --prop id=Heading2 --prop type=paragraph --prop font=Calibri --prop size=13pt --prop bold=true --prop color=2E75B6 --prop spaceBefore=18pt --prop spaceAfter=6pt --prop keepNext=true - -# Header with company name (default — body pages only) -officecli add report.docx / --type header --prop text="Acme Corporation" --prop type=default --prop font=Calibri --prop size=9pt --prop color=888888 --prop alignment=right - -# Step 1: Empty footer for cover page — adding type=first auto-enables differentFirstPage -# NOTE: Do NOT use `set / --prop differentFirstPage=true` — UNSUPPORTED on current CLI version -officecli add report.docx / --type footer --prop type=first --prop text="" - -# Step 2: Default footer with static "Page " text (--prop field=page is SILENTLY IGNORED — do not use) -officecli add report.docx / --type footer --prop text="Page " --prop type=default --prop alignment=center --prop size=9pt --prop font=Calibri - -# Step 3: REQUIRED — inject PAGE field via raw-set (footer[2] = default when first-page footer also exists) -officecli raw-set report.docx "/footer[2]" \ - --xpath "//w:p" \ - --action append \ - --xml ' PAGE ' - -# Watermark -officecli add report.docx / --type watermark --prop text=DRAFT --prop color=C0C0C0 --prop opacity=0.5 - -# ── Cover Page ────────────────────────────────────────────────────────────── -# REQUIRED minimum elements (content area must fill >= 60% of the page): -# 1. Top accent bar (color block) -# 2. Company / project name (14pt, above title) -# 3. Main title (28-32pt, bold) -# 4. Subtitle / document type (18-20pt) -# 5. Author / department -# 6. Date -# 7. Bottom accent bar + contact / version info -# -# Always set alignment=center and explicit font sizes on every cover element. -# Use shading bars to fill visual space and avoid large blank areas. - -# Top color accent bar -officecli add report.docx /body --type paragraph --prop text="" --prop shd=1F3864 --prop spaceBefore=0pt --prop spaceAfter=0pt --prop size=20pt -# Spacer -officecli add report.docx /body --type paragraph --prop text="" --prop spaceBefore=36pt --prop spaceAfter=0pt - -# Company / project name (element 2) -officecli add report.docx /body --type paragraph --prop text="Acme Corporation" --prop alignment=center --prop size=14pt --prop color=1F4E79 --prop spaceAfter=6pt - -# Main title (element 3) — 28-32pt -officecli add report.docx /body --type paragraph --prop text="Q4 Business Report" --prop alignment=center --prop size=30pt --prop bold=true --prop color=1F4E79 --prop spaceAfter=12pt - -# Subtitle / document type (element 4) — 18-20pt -officecli add report.docx /body --type paragraph --prop text="Fiscal Year 2025 — Annual Performance Review" --prop alignment=center --prop size=18pt --prop color=4472C4 --prop spaceAfter=36pt - -# Mid accent bar (visual separator) -officecli add report.docx /body --type paragraph --prop text="" --prop shd=4472C4 --prop spaceBefore=0pt --prop spaceAfter=24pt --prop size=8pt - -# Author / department (element 5) -officecli add report.docx /body --type paragraph --prop text="Prepared by: Team Alpha | Finance & Strategy Division" --prop alignment=center --prop size=11pt --prop color=444444 --prop spaceAfter=8pt - -# Date (element 6) -officecli add report.docx /body --type paragraph --prop text="March 2026" --prop alignment=center --prop size=11pt --prop color=444444 --prop spaceAfter=8pt - -# Version / confidentiality notice -officecli add report.docx /body --type paragraph --prop text="Version 1.0 | CONFIDENTIAL" --prop alignment=center --prop size=9pt --prop color=888888 --prop spaceAfter=36pt - -# Bottom accent bar + contact info (element 7) -officecli add report.docx /body --type paragraph --prop text="" --prop shd=1F3864 --prop spaceBefore=0pt --prop spaceAfter=6pt --prop size=12pt -officecli add report.docx /body --type paragraph --prop text="contact@acmecorp.com | www.acmecorp.com" --prop alignment=center --prop size=9pt --prop color=888888 --prop spaceAfter=0pt - -officecli add report.docx /body --type pagebreak - -# Table of Contents -officecli add report.docx /body --type toc --prop levels="1-3" --prop title="Table of Contents" --prop hyperlinks=true --prop pagenumbers=true --index 0 - -# Title and executive summary -officecli add report.docx /body --type paragraph --prop text="Q4 Business Report" --prop style=Heading1 -officecli add report.docx /body --type paragraph --prop text="Executive Summary" --prop style=Heading2 -officecli add report.docx /body --type paragraph --prop text="This report summarizes Q4 performance across all divisions. Revenue grew 25% year-over-year while operating costs decreased 12%." --prop font=Calibri --prop size=11pt --prop spaceAfter=12pt --prop lineSpacing=1.15x - -# Key highlights (bulleted list) -officecli add report.docx /body --type paragraph --prop text="Key Highlights" --prop style=Heading2 -officecli add report.docx /body --type paragraph --prop text="Revenue increased to $5.1M (+25% YoY)" --prop listStyle=bullet -officecli add report.docx /body --type paragraph --prop text="Customer retention rate reached 94%" --prop listStyle=bullet -officecli add report.docx /body --type paragraph --prop text="New market expansion on track for Q1 launch" --prop listStyle=bullet - -# Revenue section with table -officecli add report.docx /body --type paragraph --prop text="Revenue Overview" --prop style=Heading2 -officecli add report.docx /body --type table --prop rows=4 --prop cols=3 --prop width="100%" --prop style=TableGrid - -# Set header row text and flag -officecli set report.docx "/body/tbl[1]/tr[1]" --prop c1="Division" --prop c2="Q3" --prop c3="Q4" --prop header=true - -# Style header cells individually (row set does NOT support bold/shd/color) -officecli set report.docx "/body/tbl[1]/tr[1]/tc[1]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -officecli set report.docx "/body/tbl[1]/tr[1]/tc[2]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -officecli set report.docx "/body/tbl[1]/tr[1]/tc[3]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF - -# Fill data rows -officecli set report.docx "/body/tbl[1]/tr[2]" --prop c1="North America" --prop c2="$4.2M" --prop c3="$5.1M" -officecli set report.docx "/body/tbl[1]/tr[3]" --prop c1="Europe" --prop c2="$3.1M" --prop c3="$3.8M" -officecli set report.docx "/body/tbl[1]/tr[4]" --prop c1="APAC" --prop c2="$1.8M" --prop c3="$2.3M" - -# Set table borders -officecli set report.docx "/body/tbl[1]" --prop border.all="single;4;CCCCCC;0" - -# Column chart -officecli add report.docx /body --type paragraph --prop text="Revenue Trend" --prop style=Heading2 -officecli add report.docx /body --type chart --prop chartType=column --prop title="Quarterly Revenue" --prop categories="Q1,Q2,Q3,Q4" --prop series1="2024:42,58,65,78" --prop series2="2025:51,67,74,92" --prop width=15cm --prop height=10cm --prop colors=1F4E79,4472C4 --prop legend=bottom - -# Validate and close -officecli validate report.docx -officecli close report.docx -``` - ---- - -### Cover Page Design: Content-Rich Standard - -> **RULE: The cover page content area must fill at least 60% of the page. Large blank areas are a deliverability defect.** - -#### Minimum Element Checklist by Document Type - -| Document Type | Required Cover Elements | -|---|---| -| **Business Report / Annual Report** | Top accent bar · Company name · Main title (28-32pt) · Subtitle/fiscal year (18-20pt) · Author/department · Date · Version/confidentiality · Bottom bar + contact | -| **Business Proposal** | Top accent bar · Client name · Proposal title (28-32pt) · Prepared-for / prepared-by block · Date · 3-5 bullet key benefits (optional callout) · Confidentiality notice | -| **Technical Specification** | Top bar · Product/project name · Document title (28-32pt) · Version number + status (DRAFT/FINAL) · Author · Date · Target audience · Bottom bar | - -#### Cover Page Lower Half — Must Not Be >40% Empty - -A common issue is a cover page where the title block ends near the top or middle, leaving the lower half largely blank. This is a deliverability defect. After placing the title/subtitle/author/date block, check whether the lower half is filled. If not, add one or more of the following blocks before the bottom accent bar: - -**Option A — Abstract Excerpt Block** (for reports, technical specs): -```bash -# Abstract excerpt shaded block -officecli add doc.docx /body --type paragraph --prop text="" --prop shd=EBF3FB --prop size=4pt --prop spaceBefore=0pt --prop spaceAfter=0pt -officecli add doc.docx /body --type paragraph --prop text="ABSTRACT" --prop alignment=center --prop font=Calibri --prop size=9pt --prop bold=true --prop color=1F4E79 --prop spaceBefore=10pt --prop spaceAfter=6pt --prop shd=EBF3FB -officecli add doc.docx /body --type paragraph --prop text="This document presents..." --prop alignment=center --prop font=Calibri --prop size=10pt --prop italic=true --prop color=444444 --prop spaceBefore=0pt --prop spaceAfter=10pt --prop shd=EBF3FB --prop leftIndent=720 --prop rightIndent=720 -officecli add doc.docx /body --type paragraph --prop text="" --prop shd=EBF3FB --prop size=4pt --prop spaceBefore=0pt --prop spaceAfter=24pt -``` - -**Option B — Document Scope Statement** (for policy, proposal, formal reports): -```bash -officecli add doc.docx /body --type paragraph --prop text="DOCUMENT SCOPE" --prop alignment=center --prop font=Calibri --prop size=9pt --prop bold=true --prop color=888888 --prop spaceBefore=36pt --prop spaceAfter=6pt -officecli add doc.docx /body --type paragraph --prop text="This document applies to all employees of Acme Corporation and covers Q4 2025 fiscal year results." --prop alignment=center --prop font=Calibri --prop size=10pt --prop color=555555 --prop spaceAfter=8pt --prop leftIndent=720 --prop rightIndent=720 -``` - -**Option C — Key Highlights List** (for annual reports, proposals): -```bash -officecli add doc.docx /body --type paragraph --prop text="KEY HIGHLIGHTS" --prop alignment=center --prop font=Calibri --prop size=9pt --prop bold=true --prop color=1F4E79 --prop spaceBefore=36pt --prop spaceAfter=8pt -officecli add doc.docx /body --type paragraph --prop text="Revenue grew 25% year-over-year" --prop listStyle=bullet --prop font=Calibri --prop size=10pt --prop color=333333 --prop spaceAfter=4pt -officecli add doc.docx /body --type paragraph --prop text="Customer retention reached 94%" --prop listStyle=bullet --prop font=Calibri --prop size=10pt --prop color=333333 --prop spaceAfter=4pt -officecli add doc.docx /body --type paragraph --prop text="Three new markets launched" --prop listStyle=bullet --prop font=Calibri --prop size=10pt --prop color=333333 --prop spaceAfter=24pt -``` - -> **Rule: Cover page lower half must not be >40% empty.** If your title/author/date block ends in the upper 60% of the page, add Option A, B, or C above before the bottom accent bar. - -#### Filling Visual Space -Use shading bars (`--prop shd=HEX`) and explicit `spaceBefore`/`spaceAfter` on every element to distribute content across the page. Even empty paragraphs with a background color create professional visual structure: - -```bash -# Color accent bar (full-width colored strip) -officecli add doc.docx /body --type paragraph --prop text="" --prop shd=1F3864 --prop size=20pt --prop spaceBefore=0pt --prop spaceAfter=0pt - -# Thinner accent line -officecli add doc.docx /body --type paragraph --prop text="" --prop shd=4472C4 --prop size=8pt --prop spaceBefore=0pt --prop spaceAfter=16pt -``` - -#### Cover Alignment Rule -Every cover page paragraph **must** use `--prop alignment=center` (or `alignment=left` for left-aligned corporate style). Never leave cover text at default paragraph alignment. - -#### Pitfall: `pbdr` Schema Errors on Cover Elements -If you use paragraph borders (`--prop pbdr.all=...`) on cover elements and validation fails, remove the offending border: -```bash -officecli validate doc.docx -# If a pBdr element causes schema error: -officecli raw-set doc.docx /document --xpath "//w:body/w:p[N]/w:pPr/w:pBdr" --action remove -# Safe alternative: use shd (background shading) alone — it never causes schema errors. -``` - ---- - -### Recipe: Formal Letter - -```bash -officecli create letter.docx - -# Page setup -officecli set letter.docx / --prop pageWidth=12240 --prop pageHeight=15840 --prop marginTop=1440 --prop marginBottom=1440 --prop marginLeft=1440 --prop marginRight=1440 - -# Date -officecli add letter.docx /body --type paragraph --prop text="March 27, 2026" --prop alignment=right --prop spaceAfter=24pt - -# Sender address (right-aligned) -officecli add letter.docx /body --type paragraph --prop text="Jane Smith" --prop alignment=right -officecli add letter.docx /body --type paragraph --prop text="Acme Corporation" --prop alignment=right -officecli add letter.docx /body --type paragraph --prop text="123 Business Ave, Suite 400" --prop alignment=right -officecli add letter.docx /body --type paragraph --prop text="New York, NY 10001" --prop alignment=right --prop spaceAfter=24pt - -# Recipient address -officecli add letter.docx /body --type paragraph --prop text="John Doe" --prop spaceAfter=0pt -officecli add letter.docx /body --type paragraph --prop text="Partner Corp" --prop spaceAfter=0pt -officecli add letter.docx /body --type paragraph --prop text="456 Commerce St" --prop spaceAfter=0pt -officecli add letter.docx /body --type paragraph --prop text="Chicago, IL 60601" --prop spaceAfter=24pt - -# Subject line -officecli add letter.docx /body --type paragraph --prop text="RE: Partnership Agreement Q2 2026" --prop bold=true --prop spaceAfter=12pt - -# Body paragraphs -officecli add letter.docx /body --type paragraph --prop text="Dear Mr. Doe," --prop spaceAfter=12pt --prop lineSpacing=1.15x -officecli add letter.docx /body --type paragraph --prop text="Thank you for your continued partnership with Acme Corporation. We are pleased to present the updated terms for our Q2 2026 collaboration agreement." --prop spaceAfter=12pt --prop lineSpacing=1.15x -officecli add letter.docx /body --type paragraph --prop text="As discussed during our March 15th meeting, the revised pricing structure reflects a 10% volume discount applicable to all orders exceeding 500 units per quarter." --prop spaceAfter=12pt --prop lineSpacing=1.15x - -# Closing -officecli add letter.docx /body --type paragraph --prop text="Sincerely," --prop spaceAfter=36pt -officecli add letter.docx /body --type paragraph --prop text="Jane Smith" --prop bold=true -officecli add letter.docx /body --type paragraph --prop text="VP of Business Development" - -# Footnote -officecli add letter.docx "/body/p[9]" --type footnote --prop text="Volume discount applies to combined orders across all product categories." - -officecli validate letter.docx -``` - -### Recipe: Academic/Research Paper - -```bash -officecli create paper.docx -officecli open paper.docx - -# Page setup -officecli set paper.docx / --prop pageWidth=12240 --prop pageHeight=15840 --prop marginTop=1440 --prop marginBottom=1440 --prop marginLeft=1440 --prop marginRight=1440 -officecli set paper.docx / --prop defaultFont=Calibri - -# Define heading styles -officecli add paper.docx /styles --type style --prop name="Heading 1" --prop id=Heading1 --prop type=paragraph --prop font=Arial --prop size=20pt --prop bold=true --prop color=000000 --prop spaceBefore=24pt --prop spaceAfter=12pt --prop keepNext=true -officecli add paper.docx /styles --type style --prop name="Heading 2" --prop id=Heading2 --prop type=paragraph --prop font=Arial --prop size=14pt --prop bold=true --prop color=000000 --prop spaceBefore=18pt --prop spaceAfter=6pt --prop keepNext=true - -# Define custom styles -officecli add paper.docx /styles --type style --prop name="Abstract" --prop id=Abstract --prop type=paragraph --prop basedOn=Normal --prop font=Calibri --prop size=11 --prop italic=true --prop color=333333 --prop leftIndent=720 --prop rightIndent=720 --prop spaceBefore=12pt --prop spaceAfter=12pt - -officecli add paper.docx /styles --type style --prop name="Block Quote" --prop id=BlockQuote --prop type=paragraph --prop basedOn=Normal --prop font=Georgia --prop size=11 --prop italic=true --prop color=555555 --prop leftIndent=720 --prop rightIndent=720 --prop spaceBefore=12pt --prop spaceAfter=12pt - -# Title page -officecli add paper.docx /body --type paragraph --prop text="On the Convergence Properties of Iterative Gradient Methods" --prop alignment=center --prop font=Calibri --prop size=18pt --prop bold=true --prop spaceBefore=72pt --prop spaceAfter=24pt - -officecli add paper.docx /body --type paragraph --prop text="A. Researcher, B. Scientist" --prop alignment=center --prop size=12pt --prop spaceAfter=6pt -officecli add paper.docx /body --type paragraph --prop text="Department of Mathematics, University of Example" --prop alignment=center --prop size=11pt --prop italic=true --prop spaceAfter=24pt - -# Section break after title page -officecli add paper.docx /body --type section --prop type=nextPage - -# Step 1: Empty footer for title page — type=first auto-enables differentFirstPage (no separate set needed) -officecli add paper.docx / --type footer --prop type=first --prop text="" - -# Step 2: Default footer (--prop field=page is SILENTLY IGNORED — add static text only here) -officecli add paper.docx / --type footer --prop text="Page " --prop type=default --prop alignment=center --prop size=9pt - -# Step 3: REQUIRED — inject PAGE field via raw-set -officecli raw-set paper.docx "/footer[2]" \ - --xpath "//w:p" \ - --action append \ - --xml ' PAGE ' - -# Table of Contents -officecli add paper.docx /body --type toc --prop levels="1-3" --prop title="Table of Contents" --prop hyperlinks=true --prop pagenumbers=true - -# Abstract -officecli add paper.docx /body --type paragraph --prop text="Abstract" --prop style=Heading1 -officecli add paper.docx /body --type paragraph --prop text="This paper examines convergence properties of gradient descent variants in high-dimensional optimization landscapes. We prove that under mild regularity conditions, the adaptive learning rate achieves optimal convergence rates." --prop style=Abstract - -# Introduction with bookmark -officecli add paper.docx /body --type paragraph --prop text="Introduction" --prop style=Heading1 -officecli add paper.docx "/body/p[7]" --type bookmark --prop name=introduction - -officecli add paper.docx /body --type paragraph --prop text="Gradient-based optimization is fundamental to modern machine learning. Given the objective function, we seek to minimize the expected risk." --prop font=Calibri --prop size=11pt --prop spaceAfter=12pt --prop lineSpacing=1.15x - -# Footnote -officecli add paper.docx "/body/p[8]" --type footnote --prop text="See Bottou et al. (2018) for a comprehensive survey of optimization methods." - -# Display equation -officecli add paper.docx /body --type equation --prop "formula=x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}" --prop mode=display - -# Inline equation in paragraph -officecli add paper.docx /body --type paragraph --prop text="The loss function is defined as " --prop font=Calibri --prop size=11pt -officecli add paper.docx "/body/p[10]" --type equation --prop "formula=L(\theta) = \frac{1}{N}\sum_{i=1}^{N} \ell(f(x_i; \theta), y_i)" --prop mode=inline - -# Methods section with bookmark -officecli add paper.docx /body --type paragraph --prop text="Methodology" --prop style=Heading1 -officecli add paper.docx "/body/p[11]" --type bookmark --prop name=methodology - -officecli add paper.docx /body --type paragraph --prop text="Convergence Analysis" --prop style=Heading2 - -# Integral equation -officecli add paper.docx /body --type equation --prop "formula=\int_{0}^{\infty} e^{-x^2} dx = \frac{\sqrt{\pi}}{2}" --prop mode=display - -# Endnote -officecli add paper.docx "/body/p[12]" --type endnote --prop text="Full convergence proofs are provided in Appendix A." - -# Cross-reference to bookmark (internal hyperlinks require raw XML -- see L3 section) -officecli add paper.docx /body --type paragraph --prop text="As established in the Introduction," --prop font=Calibri --prop size=11pt -# NOTE: To make "Introduction" a clickable internal link, use raw-set with w:hyperlink w:anchor="introduction" - -# Bibliography with hanging indent -officecli add paper.docx /body --type paragraph --prop text="References" --prop style=Heading1 -officecli add paper.docx /body --type paragraph --prop text="Bottou, L., Curtis, F. E., & Nocedal, J. (2018). Optimization methods for large-scale machine learning. SIAM Review, 60(2), 223-311." --prop leftIndent=720 --prop hangingIndent=720 --prop font=Calibri --prop size=11pt --prop spaceAfter=6pt -officecli add paper.docx /body --type paragraph --prop text="Kingma, D. P., & Ba, J. (2015). Adam: A method for stochastic optimization. Proceedings of ICLR." --prop leftIndent=720 --prop hangingIndent=720 --prop font=Calibri --prop size=11pt --prop spaceAfter=6pt - -officecli validate paper.docx -officecli close paper.docx -``` - ---- - -## Building Blocks - -### Paragraphs & Text - -```bash -# Simple paragraph -officecli add doc.docx /body --type paragraph --prop text="Hello world" --prop font=Calibri --prop size=11pt - -# Heading -officecli add doc.docx /body --type paragraph --prop text="Chapter 1" --prop style=Heading1 -``` - -**IMPORTANT: Blank documents created with `officecli create` have no formatting for built-in styles (Heading1, Heading2, etc.). You MUST define heading styles explicitly via `/styles --type style` before using `--prop style=Heading1`, or headings will appear as unstyled text. See the Business Report and Academic Paper recipes for examples.** - -```bash -# Styled paragraph -officecli add doc.docx /body --type paragraph --prop text="Important notice" --prop bold=true --prop color=FF0000 --prop alignment=center - -# Paragraph with spacing -officecli add doc.docx /body --type paragraph --prop text="Body text here" --prop spaceBefore=12pt --prop spaceAfter=6pt --prop lineSpacing=1.15x - -# Paragraph with indent -officecli add doc.docx /body --type paragraph --prop text="Indented paragraph" --prop leftIndent=720 --prop firstLineIndent=360 - -# Hanging indent (for bibliographies) -officecli add doc.docx /body --type paragraph --prop text="Author, A. (2025). Title of work..." --prop leftIndent=720 --prop hangingIndent=720 - -# Paragraph with shading (callout box) -- shd is reliable -officecli add doc.docx /body --type paragraph --prop text="Note: This is important." --prop shd=D9EAD3 - -# Callout box with border (optional -- validate after adding; pbdr may cause schema errors in some contexts) -officecli add doc.docx /body --type paragraph --prop text="Note: This is important." --prop shd=D9EAD3 --prop pbdr.all="single;4;A9D18E;4" - -# Horizontal rule via bottom border (validate after adding) -officecli add doc.docx /body --type paragraph --prop text="" --prop pbdr.bottom="single;6;CCCCCC;1" -``` - -**NOTE: `pbdr` properties may produce schema validation errors in certain contexts. Always run `officecli validate` after adding paragraph borders. If validation fails on a pBdr element, remove it with `raw-set --xpath "//w:body/w:p[N]/w:pPr/w:pBdr" --action remove`. The `shd` (shading) property alone is always safe.** - -```bash -# Page break before paragraph -officecli add doc.docx /body --type paragraph --prop text="New Chapter" --prop style=Heading1 --prop pageBreakBefore=true -``` - -#### Code Blocks (Preformatted / Monospace Text) - -There is no dedicated `code` element type in OfficeCLI. The recommended approach is a paragraph with a monospace font, light background shading, and optional left indent — this reproduces the visual appearance of a code block in Word. - -```bash -# Single-line code block (Courier New, 10pt, light gray background, indented) -officecli add doc.docx /body --type paragraph \ - --prop text='GET /api/users HTTP/1.1' \ - --prop font='Courier New' --prop size=10pt \ - --prop indent=720 \ - --prop shd=F5F5F5 \ - --prop spaceBefore=6pt --prop spaceAfter=6pt - -# Multi-line code block: add one paragraph per line with the same styling -officecli add doc.docx /body --type paragraph \ - --prop text='POST /api/orders HTTP/1.1' \ - --prop font='Courier New' --prop size=10pt --prop indent=720 --prop shd=F5F5F5 -officecli add doc.docx /body --type paragraph \ - --prop text='Content-Type: application/json' \ - --prop font='Courier New' --prop size=10pt --prop indent=720 --prop shd=F5F5F5 -officecli add doc.docx /body --type paragraph \ - --prop text='{"orderId": "12345"}' \ - --prop font='Courier New' --prop size=10pt --prop indent=720 --prop shd=F5F5F5 --prop spaceAfter=6pt -``` - -**Tips for code blocks:** -- Use `shd=F5F5F5` (light gray) or `shd=F0F0F0` for a subtle background. The `shd` property is always reliable (unlike `pbdr` borders). -- Omit `spaceAfter` / `spaceBefore` on continuation lines so they appear as a single visual block; add spacing only on the first and last line. -- To create a reusable style, define a `Code` custom style once via `/styles --type style` and apply `--prop style=Code` to each paragraph instead of repeating all props. -- **Code block indentation pitfall:** Do NOT use consecutive spaces to simulate indentation inside code block text. Use the `ind.left` paragraph property instead: `--prop ind.left=720`. Spaces-as-indent produces `view issues` warnings ("first-line indent missing") and renders inconsistently across fonts. Example: `--prop ind.left=360` for one level, `--prop ind.left=720` for two levels. - -```bash -# Define a reusable Code style (do this once per document) -officecli add doc.docx /styles --type style \ - --prop name="Code" --prop id=Code --prop type=paragraph \ - --prop font='Courier New' --prop size=10pt \ - --prop shd=F5F5F5 --prop indent=720 \ - --prop spaceBefore=4pt --prop spaceAfter=4pt - -# Then apply it to each code paragraph -officecli add doc.docx /body --type paragraph --prop text='npm install officecli' --prop style=Code -officecli add doc.docx /body --type paragraph --prop text='officecli --version' --prop style=Code -``` - -### Runs (Inline Formatting) - -```bash -# Add run to existing paragraph -officecli add doc.docx "/body/p[1]" --type run --prop text="bold text" --prop bold=true - -# Superscript/subscript -officecli add doc.docx "/body/p[1]" --type run --prop text="2" --prop superscript=true - -# Highlighted text -officecli add doc.docx "/body/p[1]" --type run --prop text="highlighted" --prop highlight=yellow - -# Small caps -officecli add doc.docx "/body/p[1]" --type run --prop text="Small Caps" --prop smallCaps=true - -# Strikethrough -officecli add doc.docx "/body/p[1]" --type run --prop text="deleted" --prop strike=true - -# W14 text effects (Word 2010+) -officecli set doc.docx "/body/p[1]/r[1]" --prop textOutline="1pt;4472C4" -officecli set doc.docx "/body/p[1]/r[1]" --prop textFill="FF0000;0000FF" -``` - -**textFill format**: `"C1;C2[;ANGLE]"` for linear gradient, `"radial:C1;C2"` for radial, or `"COLOR"` for solid fill. Do NOT prefix with `gradient;`. - -### Lists - -```bash -# Bulleted list -officecli add doc.docx /body --type paragraph --prop text="First item" --prop listStyle=bullet -officecli add doc.docx /body --type paragraph --prop text="Second item" --prop listStyle=bullet -officecli add doc.docx /body --type paragraph --prop text="Third item" --prop listStyle=bullet - -# Numbered list -officecli add doc.docx /body --type paragraph --prop text="Step one" --prop listStyle=numbered -officecli add doc.docx /body --type paragraph --prop text="Step two" --prop listStyle=numbered -officecli add doc.docx /body --type paragraph --prop text="Step three" --prop listStyle=numbered - -# Remove list style -officecli set doc.docx "/body/p[5]" --prop listStyle=none -``` - -**WARNING**: Do not set `listStyle` on a run -- it is a paragraph-level property only. - -### Tables -- Creation & Basic Styling - -```bash -# Create table -officecli add doc.docx /body --type table --prop rows=4 --prop cols=3 --prop width="100%" --prop style=TableGrid - -# Set header row text and flag -officecli set doc.docx "/body/tbl[1]/tr[1]" --prop c1="Metric" --prop c2="Q3" --prop c3="Q4" --prop header=true - -# Style header cells (row set does NOT support bold/shd/color -- use cell level) -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[2]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[3]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF - -# Fill data rows (c1/c2/c3 text shortcuts work at row level) -officecli set doc.docx "/body/tbl[1]/tr[2]" --prop c1="Revenue" --prop c2="$4.2M" --prop c3="$5.1M" -officecli set doc.docx "/body/tbl[1]/tr[3]" --prop c1="Users" --prop c2="12,400" --prop c3="15,800" -officecli set doc.docx "/body/tbl[1]/tr[4]" --prop c1="NPS" --prop c2="72" --prop c3="81" - -# Table borders -officecli set doc.docx "/body/tbl[1]" --prop border.all="single;4;CCCCCC;0" - -# Cell-level text styling -officecli set doc.docx "/body/tbl[1]/tr[2]/tc[3]" --prop bold=true --prop color=2C5F2D - -# Add row to existing table -officecli add doc.docx "/body/tbl[1]" --type row --prop c1="New Item" --prop c2="$1.5M" --prop c3="+12%" - -# Set row height -officecli set doc.docx "/body/tbl[1]/tr[1]" --prop height=480 -officecli set doc.docx "/body/tbl[1]/tr[1]" --prop height.exact=480 - -# Table cell padding (use cell-level, NOT table-level padding) -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop padding.top=40 --prop padding.bottom=40 --prop padding.left=80 --prop padding.right=80 -``` - -**WARNING: Do NOT use table-level `--prop padding=N`. It generates invalid `tblCellMar` XML that fails schema validation. Apply padding at the cell level instead.** - -**CRITICAL: Row-level `set` only supports `height`, `height.exact`, `header`, and `c1/c2/c3...` text shortcuts.** It does NOT accept `bold`, `shd`, `color`, or `font`. All formatting must be applied at the cell level (`/body/tbl[N]/tr[M]/tc[K]`). - -### Tables -- Cell Merging & Advanced Formatting - -```bash -# Vertical merge (spanning rows) -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop vmerge=restart -officecli set doc.docx "/body/tbl[1]/tr[2]/tc[1]" --prop vmerge=continue - -# Horizontal merge (spanning columns) -- use lowercase gridspan -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop gridspan=2 - -# Cell vertical alignment -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop valign=center - -# Cell shading (solid) -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop shd=E8F0FE - -# Cell shading (gradient) -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop "shd=gradient;1F4E79;4472C4;90" - -# Cell text direction -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop textDirection=btlr - -# Cell padding (individual sides) -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop padding.top=40 --prop padding.bottom=40 - -# Diagonal cell borders -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop "border.tl2br=single;4;000000;0" -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop "border.tr2bl=single;4;000000;0" - -# Individual cell borders -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop "border.bottom=single;6;1F4E79;0" -``` - -### Images - -```bash -# Inline image in body -officecli add doc.docx /body --type picture --prop path=photo.jpg --prop width=15cm --prop height=10cm --prop alt="Team photo" - -# Image in paragraph (inline with text) -officecli add doc.docx "/body/p[3]" --type picture --prop path=icon.png --prop width=1cm --prop height=1cm --prop alt="Check icon" - -# Image from URL -officecli add doc.docx /body --type picture --prop path=https://example.com/logo.png --prop width=5cm --prop height=3cm --prop alt="Company logo" - -# Floating/anchored image -officecli add doc.docx /body --type picture --prop path=sidebar.png --prop width=5cm --prop height=8cm --prop anchor=true --prop wrap=square --prop alt="Sidebar graphic" - -# Image in table cell -officecli add doc.docx "/body/tbl[1]/tr[1]/tc[1]" --type picture --prop path=avatar.jpg --prop width=2cm --prop height=2cm --prop alt="User avatar" - -# Replace existing image -officecli set doc.docx "/body/p[5]/r[1]" --prop path=new-photo.jpg -``` - -### Charts - -```bash -# Column chart -officecli add doc.docx /body --type chart --prop chartType=column --prop title="Quarterly Revenue" --prop categories="Q1,Q2,Q3,Q4" --prop series1="2024:42,58,65,78" --prop series2="2025:51,67,74,92" --prop width=15cm --prop height=10cm --prop colors=1F4E79,4472C4 - -# Pie chart -officecli add doc.docx /body --type chart --prop chartType=pie --prop title="Market Share" --prop categories="Product A,Product B,Product C" --prop data="Share:40,35,25" --prop colors=1F4E79,4472C4,A9D18E --prop dataLabels=percent --prop legend=right - -# Line chart -officecli add doc.docx /body --type chart --prop chartType=line --prop title="Trend" --prop categories="Jan,Feb,Mar,Apr,May,Jun" --prop series1="Revenue:10,15,13,20,22,28" --prop legend=bottom - -# Bar chart (horizontal) -officecli add doc.docx /body --type chart --prop chartType=bar --prop categories="US,EU,APAC" --prop data="Sales:30,40,25" - -# Doughnut chart -officecli add doc.docx /body --type chart --prop chartType=doughnut --prop categories="Complete,Remaining" --prop data="Progress:75,25" --prop colors=2C5F2D,E8E8E8 - -# Combo chart (bar + line) -officecli add doc.docx /body --type chart --prop chartType=combo --prop categories="Q1,Q2,Q3,Q4" --prop series1="Revenue:100,200,150,300" --prop series2="Growth:10,15,12,25" --prop comboSplit=1 --prop secondary=2 - -# Radar chart -officecli add doc.docx /body --type chart --prop chartType=radar --prop categories="Quality,Speed,Cost,Innovation,Support" --prop data="Score:8,7,6,9,8" - -# Stacked column -officecli add doc.docx /body --type chart --prop chartType=columnStacked --prop categories="Q1,Q2,Q3,Q4" --prop series1="Product A:10,20,15,25" --prop series2="Product B:8,12,18,22" --prop colors=1F4E79,4472C4 - -# Scatter chart -officecli add doc.docx /body --type chart --prop chartType=scatter --prop categories="1,2,3,4,5" --prop data="Values:10,25,18,30,22" -``` - -**Chart types:** column, columnStacked, bar, barStacked, line, lineStacked, pie, pie3d, doughnut, area, areaStacked, scatter, bubble, radar, stock, combo, column3d, bar3d, line3d, area3d - -**WARNING**: Chart series cannot be added after creation. Include all series in the `add` command. To change series count, delete and recreate. - -**WARNING — LibreOffice PDF: Do NOT use `chartType=pie` or `chartType=doughnut` when the output will be delivered as a LibreOffice PDF.** These chart types render without visible slices in LibreOffice PDF export — only labels and the legend appear; the actual slices are invisible. Use `chartType=column` or `chartType=bar` as a reliable replacement. Both pie and doughnut charts render correctly in Microsoft Word. - -### Equations - -```bash -# Display equation (own paragraph) -officecli add doc.docx /body --type equation --prop "formula=E = mc^2" --prop mode=display - -# Inline equation (within paragraph) -officecli add doc.docx "/body/p[3]" --type equation --prop "formula=x^2 + y^2 = r^2" --prop mode=inline - -# Quadratic formula -officecli add doc.docx /body --type equation --prop "formula=x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}" --prop mode=display - -# Integral -officecli add doc.docx /body --type equation --prop "formula=\int_{0}^{\infty} e^{-x^2} dx = \frac{\sqrt{\pi}}{2}" --prop mode=display - -# Sum -officecli add doc.docx /body --type equation --prop "formula=\sum_{n=1}^{N} n = \frac{N(N+1)}{2}" --prop mode=display - -# Matrix -officecli add doc.docx /body --type equation --prop "formula=\begin{pmatrix} a & b \\ c & d \end{pmatrix}" --prop mode=display - -# Set equation on paragraph (replaces content with display math) -officecli set doc.docx "/body/p[10]" --prop "formula=\nabla \times \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t}" - -# Inline equation via run -officecli set doc.docx "/body/p[5]/r[2]" --prop "formula=\alpha + \beta = \gamma" -``` - -**LaTeX subset reference**: `\frac{}{}`, `\sqrt{}`, `\sum`, `\int`, `\lim`, `\nabla`, `\partial`, Greek letters (`\alpha`, `\beta`, etc.), subscripts (`_`), superscripts (`^`), `\binom{}{}`, `\rightarrow`, `\rightleftharpoons`, `\pm`, `\times`, `\cdot`, `\infty`, `\begin{pmatrix}...\end{pmatrix}` - -**Equation caveats:** -- `\mathcal` is NOT reliably supported -- it generates invalid `m:scr` XML. Use `\mathit{L}` or plain italic letters instead. -- After adding equations, immediately verify with `view text` -- equations appear as `[Equation]` markers. If the marker is missing, the equation was not created correctly. -- When fixing validation errors or removing empty paragraphs, re-check that `[Equation]` markers are still present. Equation paragraphs (oMathPara) share the paragraph index space and can be accidentally deleted. - -### Hyperlinks - -```bash -# External hyperlink in paragraph -officecli add doc.docx "/body/p[1]" --type hyperlink --prop url=https://example.com --prop text="Visit our website" --prop font=Calibri --prop size=11pt - -# Make existing run a hyperlink -officecli set doc.docx "/body/p[3]/r[1]" --prop link=https://example.com - -# Remove hyperlink from run -officecli set doc.docx "/body/p[3]/r[1]" --prop link=none -``` - -### Bookmarks & Internal Links - -```bash -# Add bookmark at a paragraph -officecli add doc.docx "/body/p[5]" --type bookmark --prop name=chapter1 --prop text="Chapter 1: Introduction" - -# Rename bookmark -officecli set doc.docx "/bookmark[chapter1]" --prop name=intro - -# Replace bookmark content -officecli set doc.docx "/bookmark[chapter1]" --prop text="Updated Chapter Title" -``` - -### Footnotes & Endnotes - -```bash -# Add footnote to paragraph -officecli add doc.docx "/body/p[3]" --type footnote --prop text="Source: Annual Report 2025" - -# Add endnote -officecli add doc.docx "/body/p[5]" --type endnote --prop text="See appendix for methodology" - -# Edit existing footnote -officecli set doc.docx "/footnote[1]" --prop text="Updated source reference" -``` - -### Headers & Footers - -```bash -# Default header -officecli add doc.docx / --type header --prop text="Acme Corporation" --prop type=default --prop font=Calibri --prop size=9pt --prop color=888888 --prop alignment=right - -# First page header (different from default) -officecli add doc.docx / --type header --prop text="CONFIDENTIAL" --prop type=first --prop bold=true --prop color=FF0000 --prop alignment=center - -# Default footer with page number — requires 2-step pattern: -# Step 1: Add footer with static "Page " text (--prop field=page is SILENTLY IGNORED) -officecli add doc.docx / --type footer --prop text="Page " --prop type=default --prop alignment=center --prop size=9pt -# Step 2: REQUIRED — inject PAGE field via raw-set (footer[1] when no first-page footer) -officecli raw-set doc.docx "/footer[1]" \ - --xpath "//w:p" \ - --action append \ - --xml ' PAGE ' -``` - -> **⚠️ Known CLI bug:** `--prop field=page` is **silently ignored** in `add --type footer`. You must always use the `raw-set` step to inject the `` PAGE field. Verify with `officecli get doc.docx "/footer[N]" --depth 3` — output must show `fldChar` children. - -#### First-Page Footer (Suppress Cover Page Number) - -To make the first page footer different from all other pages, add a `type=first` footer. The CLI **automatically** inserts the required `` XML element — no separate command needed. - -> **⚠️ Known CLI limitation:** `set / --prop differentFirstPage=true` is **UNSUPPORTED** on current CLI version (silently returns "UNSUPPORTED props"). Do not use it. Just add the `type=first` footer directly — that is sufficient. - -```bash -# Step 1: Add first-page footer (empty — suppresses page number on cover) -officecli add doc.docx / --type footer --prop type=first --prop text="" - -# Step 2: Add default footer with static "Page " text (field=page is SILENTLY IGNORED) -officecli add doc.docx / --type footer --prop text="Page " --prop type=default --prop alignment=center --prop size=9pt - -# Step 3: REQUIRED — inject PAGE field via raw-set (footer[2] = default when first-page footer exists) -officecli raw-set doc.docx "/footer[2]" \ - --xpath "//w:p" \ - --action append \ - --xml ' PAGE ' -``` - -#### Composite Footer (Company Name + Page Number) - -For corporate documents with a footer like "Acme Corporation | Confidential · Page 1", each `add / --type footer --prop type=default` call appends a new paragraph inside the same footer region. Use this to build multi-line footers, or keep everything in one command for a single-line footer. - -```bash -# Single-line footer: text only (no page number) -officecli add doc.docx / --type footer --prop type=default \ - --prop text="Acme Corporation | Confidential" --prop alignment=left --prop size=9pt --prop font=Calibri - -# Two-line footer: company name on left, page number centered below -# Line 1 — company name (left-aligned) -officecli add doc.docx / --type footer --prop type=default \ - --prop text="Acme Corporation | Confidential" --prop alignment=left --prop size=9pt --prop font=Calibri -# Line 2 — add static "Page " text (field=page is SILENTLY IGNORED) -officecli add doc.docx / --type footer --prop text="Page " \ - --prop type=default --prop alignment=center --prop size=9pt --prop font=Calibri -# REQUIRED: inject PAGE field into the last paragraph of footer[1] -# Note: each add --type=default appends a paragraph to the same footer; use the correct XPath -officecli raw-set doc.docx "/footer[1]" \ - --xpath "(//w:p)[last()]" \ - --action append \ - --xml ' PAGE ' -``` - -```bash -# Edit header text -officecli set doc.docx "/header[1]" --prop text="Updated Header" -``` - -Header/footer types: `default`, `first`, `even` - -### Watermarks - -```bash -# Add text watermark -officecli add doc.docx / --type watermark --prop text=DRAFT --prop color=C0C0C0 --prop font=Calibri --prop opacity=0.5 --prop rotation=315 - -# Modify watermark -officecli set doc.docx /watermark --prop text=CONFIDENTIAL --prop color=FF0000 -``` - -**Note**: Default rotation is `315` degrees. Use positive degree values, not negative. - -### Sections & Page Layout - -```bash -# Add section break (next page) -officecli add doc.docx /body --type section --prop type=nextPage - -# Continuous section break (for column changes) -officecli add doc.docx /body --type section --prop type=continuous - -# Set section to landscape (note: section uses lowercase pagewidth/pageheight) -officecli set doc.docx "/section[2]" --prop orientation=landscape --prop pagewidth=15840 --prop pageheight=12240 - -# Multi-column section -officecli set doc.docx "/section[2]" --prop columns=2 --prop separator=true - -# Custom column widths -officecli set doc.docx "/section[2]" --prop columns=2 --prop "colWidths=5400,3600" - -# Even/odd page section break -officecli add doc.docx /body --type section --prop type=evenPage - -# Section margins (lowercase) -officecli set doc.docx "/section[2]" --prop margintop=1440 --prop marginbottom=1440 -``` - -**CRITICAL**: Section properties use lowercase names (`pagewidth`, `pageheight`, `margintop`, etc.). Document root (`/`) uses camelCase (`pageWidth`, `pageHeight`, `marginTop`, etc.). Do not confuse the two. - -### Page Breaks & Column Breaks - -```bash -# Page break -officecli add doc.docx /body --type pagebreak - -# Page break within paragraph -officecli add doc.docx "/body/p[5]" --type break --prop type=page - -# Column break -officecli add doc.docx "/body/p[10]" --type break --prop type=column -``` - -### Fields - -```bash -# Page number field -officecli add doc.docx "/body/p[1]" --type pagenum - -# Total pages field -officecli add doc.docx "/body/p[1]" --type numpages - -# Date field -officecli add doc.docx "/body/p[1]" --type date - -# Custom date format -officecli add doc.docx "/body/p[1]" --type field --prop instruction=" DATE \\@ \"yyyy-MM-dd\" " --prop text="2026-01-01" - -# Author field -officecli add doc.docx "/body/p[1]" --type field --prop fieldType=author - -# Field at body level (creates paragraph) -officecli add doc.docx /body --type pagenum --prop alignment=center -``` - -### Comments - -```bash -# Add comment to paragraph -officecli add doc.docx "/body/p[3]" --type comment --prop text="Please review this section" --prop author="Claude" --prop initials="C" - -# Add comment to specific run -officecli add doc.docx "/body/p[3]/r[1]" --type comment --prop text="Is this figure correct?" --prop author="Claude" -``` - -### Table of Contents - -> **REQUIRED: If a document has 3 or more level-1 headings, you MUST add a TOC.** This is a hard rule — do not skip it. - -```bash -# Add TOC immediately after the cover page break, at index 0 (before all body content) -# Use --index 0 so the TOC appears at the top of the body, not at the end -officecli add doc.docx /body --type toc --prop levels="1-3" --prop title="Table of Contents" --prop hyperlinks=true --prop pagenumbers=true --index 0 - -# Modify TOC depth -officecli set doc.docx "/toc[1]" --prop levels="1-4" -``` - -**TOC display note:** After adding a TOC with OfficeCLI, the TOC shows as a field code placeholder in `view text` output — this is expected. In Microsoft Word, press **F9** to update and render the TOC with actual page numbers. The TOC entries and links are fully functional once rendered in Word. - -### Content Controls (SDT) - -```bash -# Text content control -officecli add doc.docx /body --type sdt --prop sdtType=text --prop alias="Company Name" --prop tag=company --prop text="Enter company name" - -# Rich text content control -officecli add doc.docx /body --type sdt --prop sdtType=richtext --prop alias="Description" --prop tag=description --prop text="Enter description" - -# Dropdown -officecli add doc.docx /body --type sdt --prop sdtType=dropdown --prop alias="Status" --prop tag=status --prop "items=Draft,In Review,Final" - -# Date picker -officecli add doc.docx /body --type sdt --prop sdtType=date --prop alias="Due Date" --prop tag=duedate --prop format="MM/dd/yyyy" - -# Combobox (editable dropdown) -officecli add doc.docx /body --type sdt --prop sdtType=combobox --prop alias="Department" --prop tag=dept --prop "items=Engineering,Marketing,Sales,HR" - -# Locked content control -officecli add doc.docx /body --type sdt --prop sdtType=richtext --prop lock=contentlocked --prop text="Protected content" - -# Inline SDT within paragraph -officecli add doc.docx "/body/p[1]" --type sdt --prop sdtType=text --prop alias="Inline Field" --prop text="fill in" -``` - -SDT types: `text`, `richtext`, `dropdown`, `combobox`, `date` - -### Custom Styles - -```bash -# Create paragraph style -officecli add doc.docx /styles --type style --prop name="Block Quote" --prop id=BlockQuote --prop type=paragraph --prop basedOn=Normal --prop font=Georgia --prop size=11 --prop italic=true --prop color=555555 --prop leftIndent=720 --prop rightIndent=720 --prop spaceBefore=12pt --prop spaceAfter=12pt - -# Create character style -officecli add doc.docx /styles --type style --prop name="Emphasis Bold" --prop id=EmphasisBold --prop type=character --prop bold=true --prop color=1F4E79 - -# Apply custom style -officecli set doc.docx "/body/p[10]" --prop style=BlockQuote -``` - -### Find/Replace - -```bash -# Find and replace across entire document -officecli set doc.docx / --prop find="2024" --prop replace="2025" - -# Scoped find/replace (body only, not headers/footers) -officecli set doc.docx / --prop find="old text" --prop replace="new text" --prop scope=body - -# Replace in headers/footers only -officecli set doc.docx / --prop find="Company Name" --prop replace="Acme Corp" --prop scope=headers -``` - -**WARNING: Find/replace performs substring matching, not whole-word matching. Replacing "ACME" in "ACME Corporation" produces "New Name Corporation". After any find/replace, review with `view text` and run a second cleanup pass if needed.** - -### Track Changes - -```bash -# Accept all tracked changes -officecli set doc.docx / --prop accept-changes=all - -# Reject all tracked changes -officecli set doc.docx / --prop reject-changes=all -``` - -**WARNING**: Creating tracked changes (insertions/deletions with author markup) is NOT supported via high-level commands. Use `raw-set` with XML. See L3 escalation section below. - -### Clone Elements - -```bash -# Clone a paragraph -officecli add doc.docx /body --from "/body/p[1]" - -# Clone a table -officecli add doc.docx /body --from "/body/tbl[1]" -``` - -### Remove / Move / Swap - -```bash -# Remove element -officecli remove doc.docx "/body/p[5]" - -# Move element -officecli move doc.docx "/body/p[5]" --index 0 - -# Swap elements -officecli swap doc.docx "/body/p[1]" "/body/p[3]" -``` - ---- - -## Advanced Features - -### Batch Set with Selectors - -```bash -# Set font on all Heading1 paragraphs -officecli set doc.docx 'paragraph[style=Heading1]' --prop font=Georgia --prop color=1F4E79 - -# Bold all paragraphs containing "important" -officecli set doc.docx 'p:contains("important")' --prop bold=true - -# Find all images missing alt text (query first, then set individually) -officecli query doc.docx 'image:no-alt' -``` - -### L1, L2, L3 Escalation (When to Use Raw XML) - -**L1 -- High-level commands (use first)**: -- `add`, `set`, `get`, `query`, `remove`, `move`, `swap` -- Covers 90% of use cases - -**L2 -- Batch with selectors**: -- `set doc.docx 'selector' --prop key=value` -- For bulk modifications across document - -**L3 -- Raw XML (last resort)**: -- `raw` to inspect XML -- `raw-set` to modify XML directly -- `add-part` to create new document parts (returns rId) -- **Use for**: tracked change creation, tab stops, complex numbering definitions, advanced SmartArt, anything not exposed by L1/L2 - -```bash -# View raw XML of document body -officecli raw doc.docx /document - -# View raw XML of styles -officecli raw doc.docx /styles - -# View raw XML of numbering -officecli raw doc.docx /numbering - -# Modify XML attribute -officecli raw-set doc.docx /document --xpath "//w:body/w:p[1]/w:pPr/w:jc" --action setattr --xml "w:val=center" - -# Append XML element (e.g., tab stops) -officecli raw-set doc.docx /document --xpath "//w:body/w:p[1]/w:pPr" --action append --xml '' - -# Remove XML element -officecli raw-set doc.docx /document --xpath "//w:body/w:p[3]" --action remove - -# Internal hyperlink via raw XML (link to bookmark named "methodology") -officecli raw-set doc.docx /document --xpath "//w:body/w:p[14]" --action append --xml 'Methodology' - -# Create tracked change via raw XML (insertion) -officecli raw-set doc.docx /document --xpath "//w:body/w:p[5]" --action append --xml 'inserted text' - -# Add new document part -officecli add-part doc.docx /document -``` - -**Raw XML parts**: /document, /styles, /numbering, /settings, /header[N], /footer[N], /comments, /chart[N] - -**XPath prefixes**: w (WordprocessingML), r (Relationships), a (DrawingML), mc (Markup Compatibility), wp (Word Drawing) - -**raw-set actions**: append, prepend, insertbefore, insertafter, replace, remove, setattr - ---- - -## More Recipes - -### Complete Business Report - -```bash -officecli set doc.docx / --prop title="Q4 Business Report" --prop author="Team Alpha" -officecli add doc.docx /body --type paragraph --prop text="Q4 Business Report" --prop style=Heading1 -officecli add doc.docx /body --type paragraph --prop text="This report summarizes Q4 performance across all divisions." --prop font=Calibri --prop size=11pt --prop spaceAfter=12pt -officecli add doc.docx /body --type paragraph --prop text="Revenue Overview" --prop style=Heading2 -officecli add doc.docx /body --type paragraph --prop text="Total revenue increased 25% year-over-year." --prop font=Calibri --prop size=11pt -officecli add doc.docx /body --type table --prop rows=3 --prop cols=3 --prop width=100% --prop style=TableGrid -officecli set doc.docx "/body/tbl[1]/tr[1]" --prop c1=Division --prop c2=Q3 --prop c3=Q4 --prop header=true -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[2]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[3]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[1]/tr[2]" --prop c1="North America" --prop c2='$4.2M' --prop c3='$5.1M' -officecli set doc.docx "/body/tbl[1]/tr[3]" --prop c1=Europe --prop c2='$3.1M' --prop c3='$3.8M' -``` - -### Table with Merged Headers - -```bash -officecli add doc.docx /body --type table --prop rows=4 --prop cols=4 --prop width=100% --prop style=TableGrid -officecli set doc.docx "/body/tbl[1]/tr[1]" --prop c1=Category --prop c2=2024 --prop c3="" --prop c4="" -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[2]" --prop gridspan=3 --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF --prop alignment=center -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop vmerge=restart --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF --prop valign=center -officecli set doc.docx "/body/tbl[1]/tr[2]" --prop c1="" --prop c2=Q1 --prop c3=Q2 --prop c4=Q3 -officecli set doc.docx "/body/tbl[1]/tr[2]/tc[1]" --prop vmerge=continue -officecli set doc.docx "/body/tbl[1]/tr[2]/tc[2]" --prop bold=true --prop shd=4472C4 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[1]/tr[2]/tc[3]" --prop bold=true --prop shd=4472C4 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[1]/tr[2]/tc[4]" --prop bold=true --prop shd=4472C4 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[1]/tr[3]" --prop c1=Revenue --prop c2='$4.2M' --prop c3='$5.1M' --prop c4='$5.8M' -officecli set doc.docx "/body/tbl[1]/tr[4]" --prop c1=Users --prop c2=12K --prop c3=15K --prop c4=18K -``` - -### Multi-Element Paragraph - -```bash -officecli add doc.docx /body --type paragraph --prop text="" -officecli add doc.docx /body/p[1] --type run --prop text="Important: " --prop bold=true --prop color=FF0000 --prop font=Calibri --prop size=11pt -officecli add doc.docx /body/p[1] --type run --prop text="This deadline is " --prop font=Calibri --prop size=11pt -officecli add doc.docx /body/p[1] --type run --prop text="March 31, 2026" --prop bold=true --prop underline=single --prop font=Calibri --prop size=11pt -officecli add doc.docx /body/p[1] --type run --prop text=". Please submit all documents before this date." --prop font=Calibri --prop size=11pt -``` - ---- - -## Document Closing (Last Page) - -> **RULE: The last page must have content filling at least 40% of the page. A near-empty final page signals an unfinished document.** - -Every professional document needs a deliberate closing section. Never end a document with a sparse final paragraph — always add one of the following closing patterns. - -### Closing Pattern A: Full Closing Section (Recommended for Reports & Proposals) - -Include Conclusion / Summary, Next Steps (if applicable), and contact information. This pattern reliably fills the closing page. - -```bash -# Conclusion section heading -officecli add doc.docx /body --type paragraph --prop text="Conclusion" --prop style=Heading1 - -# Conclusion summary text -officecli add doc.docx /body --type paragraph --prop text="This document has outlined the key findings and recommendations for Q4. The data demonstrates strong performance across all divisions, with particular strength in the APAC region." --prop font=Calibri --prop size=11pt --prop spaceAfter=12pt --prop lineSpacing=1.15x - -# Next steps (if applicable) -officecli add doc.docx /body --type paragraph --prop text="Next Steps" --prop style=Heading2 -officecli add doc.docx /body --type paragraph --prop text="Finalize Q1 budget allocation by April 15" --prop listStyle=numbered -officecli add doc.docx /body --type paragraph --prop text="Present findings to the board on April 20" --prop listStyle=numbered -officecli add doc.docx /body --type paragraph --prop text="Launch APAC expansion pilot in May 2026" --prop listStyle=numbered - -# Contact / acknowledgements section -officecli add doc.docx /body --type paragraph --prop text="Contact Information" --prop style=Heading2 -officecli add doc.docx /body --type paragraph --prop text="For questions or follow-up, please contact:" --prop font=Calibri --prop size=11pt --prop spaceAfter=6pt -officecli add doc.docx /body --type paragraph --prop text="Team Alpha — Finance & Strategy" --prop font=Calibri --prop size=11pt --prop bold=true --prop spaceAfter=0pt -officecli add doc.docx /body --type paragraph --prop text="Email: team.alpha@acmecorp.com" --prop font=Calibri --prop size=11pt --prop spaceAfter=0pt -officecli add doc.docx /body --type paragraph --prop text="Phone: +1 (212) 555-0100" --prop font=Calibri --prop size=11pt --prop spaceAfter=24pt - -# Bottom accent bar + legal notice -officecli add doc.docx /body --type paragraph --prop text="" --prop shd=1F3864 --prop size=8pt --prop spaceBefore=0pt --prop spaceAfter=8pt -officecli add doc.docx /body --type paragraph --prop text="© 2026 Acme Corporation. All rights reserved. This document is confidential and intended solely for the named recipients." --prop font=Calibri --prop size=9pt --prop color=888888 --prop alignment=center --prop spaceAfter=0pt -``` - -### Closing Pattern B: Minimal Closing Page (Letters, Memos, Short Reports) - -When content naturally ends early, add a "Thank You" close plus contact information to reach the 40% threshold. - -```bash -# Closing statement -officecli add doc.docx /body --type paragraph --prop text="Thank You" --prop alignment=center --prop font=Calibri --prop size=24pt --prop bold=true --prop color=1F4E79 --prop spaceBefore=48pt --prop spaceAfter=16pt - -# Subtitle line -officecli add doc.docx /body --type paragraph --prop text="We appreciate your time and look forward to the next steps." --prop alignment=center --prop font=Calibri --prop size=12pt --prop color=444444 --prop spaceAfter=36pt - -# Accent divider -officecli add doc.docx /body --type paragraph --prop text="" --prop shd=4472C4 --prop size=6pt --prop spaceBefore=0pt --prop spaceAfter=24pt - -# Contact block -officecli add doc.docx /body --type paragraph --prop text="contact@acmecorp.com | www.acmecorp.com | +1 (212) 555-0100" --prop alignment=center --prop font=Calibri --prop size=10pt --prop color=666666 --prop spaceAfter=8pt - -# Document version/date footer line -officecli add doc.docx /body --type paragraph --prop text="Document Version 1.0 — March 2026" --prop alignment=center --prop font=Calibri --prop size=9pt --prop color=AAAAAA --prop spaceAfter=0pt -``` - -### Closing Pattern C: Appendix + Version History (Technical Spec / Formal Reports) - -```bash -# Appendix section -officecli add doc.docx /body --type paragraph --prop text="Appendix" --prop style=Heading1 - -officecli add doc.docx /body --type paragraph --prop text="A. Glossary" --prop style=Heading2 -officecli add doc.docx /body --type paragraph --prop text="API — Application Programming Interface" --prop leftIndent=720 --prop hangingIndent=360 --prop font=Calibri --prop size=11pt --prop spaceAfter=4pt -officecli add doc.docx /body --type paragraph --prop text="CI/CD — Continuous Integration / Continuous Deployment" --prop leftIndent=720 --prop hangingIndent=360 --prop font=Calibri --prop size=11pt --prop spaceAfter=4pt - -officecli add doc.docx /body --type paragraph --prop text="B. Version History" --prop style=Heading2 -officecli add doc.docx /body --type table --prop rows=4 --prop cols=3 --prop width="100%" --prop style=TableGrid -officecli set doc.docx "/body/tbl[last]/tr[1]" --prop c1="Version" --prop c2="Date" --prop c3="Changes" --prop header=true -officecli set doc.docx "/body/tbl[last]/tr[1]/tc[1]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[last]/tr[1]/tc[2]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[last]/tr[1]/tc[3]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -officecli set doc.docx "/body/tbl[last]/tr[2]" --prop c1="1.0" --prop c2="2026-03-01" --prop c3="Initial draft" -officecli set doc.docx "/body/tbl[last]/tr[3]" --prop c1="1.1" --prop c2="2026-03-15" --prop c3="Review comments incorporated" -officecli set doc.docx "/body/tbl[last]/tr[4]" --prop c1="1.2" --prop c2="2026-03-27" --prop c3="Final approved version" - -# Legal notice -officecli add doc.docx /body --type paragraph --prop text="" --prop shd=1F3864 --prop size=8pt --prop spaceBefore=36pt --prop spaceAfter=8pt -officecli add doc.docx /body --type paragraph --prop text="© 2026 Acme Corporation. Confidential. Do not distribute without written permission." --prop font=Calibri --prop size=9pt --prop color=888888 --prop alignment=center -``` - -### Pre-Delivery: Last-Page Density Check - -After completing your document, always verify the final page is not sparse: - -```bash -# Check total page structure -officecli view doc.docx outline - -# Read the last section of content -officecli view doc.docx text --start -30 -``` - -If the last page has fewer than 3-4 substantive paragraphs, add a Closing Pattern (A, B, or C above) before delivering. diff --git a/skills/officecli-docx/editing.md b/skills/officecli-docx/editing.md deleted file mode 100644 index eb5600889..000000000 --- a/skills/officecli-docx/editing.md +++ /dev/null @@ -1,346 +0,0 @@ - - -# Editing Existing Documents - -Use this guide when modifying an existing .docx file (template-based or content update). - -## Workflow Overview - -1. **Analyze** the document (structure, styles, content) -2. **Plan** content mapping (what to change, what to keep) -3. **Structural changes** (add/remove sections, reorder elements) -- **do this FIRST** -4. **Content edits** (text, images, charts, tables) -5. **QA** (content + validation) -- see [SKILL.md](SKILL.md#qa-required) - ---- - -## Analyzing the Document - -### Step 1: Issue Detection - -```bash -officecli view doc.docx issues -``` - -Start with issues to understand existing problems before making changes. - -### Step 2: Structure Overview - -```bash -officecli view doc.docx outline -``` - -Look for: heading hierarchy, section count, headers/footers presence, watermarks. - -### Step 3: Content Extraction - -```bash -# Full text with element paths -officecli view doc.docx text - -# Limit output for large documents -officecli view doc.docx text --max-lines 100 - -# Annotated view (formatting details) -officecli view doc.docx annotated -``` - -### Step 4: Style & Font Analysis - -```bash -officecli view doc.docx stats -``` - -Use stats to understand the document's style palette before making changes. Match existing styles rather than introducing inline formatting. - -### Step 5: Element Inspection - -```bash -# Document-level properties -officecli get doc.docx / - -# Body structure at depth 1 -officecli get doc.docx /body --depth 1 - -# Specific table -officecli get doc.docx "/body/tbl[1]" --depth 3 - -# Styles in use -officecli get doc.docx /styles - -# Specific style definition -officecli get doc.docx "/styles/Heading1" - -# Header/footer content -officecli get doc.docx "/header[1]" -officecli get doc.docx "/footer[1]" -``` - -### Step 6: Find Specific Elements - -```bash -officecli query doc.docx 'paragraph[style=Heading1]' -officecli query doc.docx 'p:contains("quarterly")' -officecli query doc.docx 'image:no-alt' -officecli query doc.docx 'p:empty' -``` - ---- - -## Planning Content Mapping - -Before making changes, plan which elements to keep, modify, or remove: - -``` -Source content Action --------------- ------ -Title paragraph -> Update text -Executive summary -> Rewrite paragraph content -Revenue table -> Update data in rows 2-4 -Q3 chart -> Delete and recreate with Q4 data -Header -> Update company name -Footer -> Keep (page numbers) -Appendix section -> Remove entirely -New conclusions section -> Add after main body -``` - ---- - -## Structural Changes (Do First) - -**WARNING**: Complete ALL structural changes before editing content. Structural changes shift element indices. - -### Add/Remove Paragraphs - -```bash -# Add paragraph at specific position -officecli add doc.docx /body --type paragraph --prop text="New section" --prop style=Heading1 --index 5 - -# Remove paragraph (highest index first if removing multiple) -officecli remove doc.docx "/body/p[15]" -officecli remove doc.docx "/body/p[10]" -``` - -**Remove from highest index to lowest** to avoid index shifting problems. - -### Add/Remove Sections - -```bash -# Add section break -officecli add doc.docx /body --type section --prop type=nextPage --index 12 -``` - -### Reorder Elements - -```bash -# Move paragraph to position -officecli move doc.docx "/body/p[8]" --index 2 - -# Swap two paragraphs -officecli swap doc.docx "/body/p[3]" "/body/p[7]" -``` - -### Re-query After Structural Changes - -After any add/remove/move operation, indices shift. Always re-query: - -```bash -officecli get doc.docx /body --depth 1 -``` - ---- - -## Content Editing - -### Modify Text - -```bash -# Set paragraph text -officecli set doc.docx "/body/p[1]" --prop text="Updated Title" - -# Set text with formatting -officecli set doc.docx "/body/p[3]" --prop text="New body content" --prop font=Calibri --prop size=11pt - -# Multi-line text -officecli set doc.docx "/body/p[5]" --prop "text=First point\\nSecond point\\nThird point" - -# Set specific run text -officecli set doc.docx "/body/p[3]/r[2]" --prop text="modified phrase" --prop bold=true -``` - -### Update Tables - -```bash -# Update row data (text shortcuts only at row level) -officecli set doc.docx "/body/tbl[1]/tr[2]" --prop c1="Updated" --prop c2="$5.5M" --prop c3="$6.2M" - -# Update individual cell with formatting -officecli set doc.docx "/body/tbl[1]/tr[2]/tc[3]" --prop text="$6.2M" --prop bold=true --prop color=2C5F2D - -# Add new row -officecli add doc.docx "/body/tbl[1]" --type row --prop c1="Totals" --prop c2="$10.5M" --prop c3="$12.1M" - -# Style the new row's cells -officecli set doc.docx "/body/tbl[1]/tr[5]/tc[1]" --prop bold=true -officecli set doc.docx "/body/tbl[1]/tr[5]/tc[2]" --prop bold=true -officecli set doc.docx "/body/tbl[1]/tr[5]/tc[3]" --prop bold=true - -# Remove row (remove from highest index first) -officecli remove doc.docx "/body/tbl[1]/tr[5]" -``` - -**CRITICAL**: Row-level `set` only supports `height`, `height.exact`, `header`, and `c1/c2/c3...` text shortcuts. For formatting (bold, shd, color, font, alignment), use cell-level `set` on each `tc[N]` individually. - -### Replace Images - -```bash -# Replace image file -officecli set doc.docx "/body/p[5]/r[1]" --prop path=new-image.jpg - -# Update image dimensions -officecli set doc.docx "/body/p[5]/r[1]" --prop width=12cm --prop height=8cm - -# Update alt text -officecli set doc.docx "/body/p[5]/r[1]" --prop alt="Updated chart screenshot" -``` - -### Update Headers/Footers - -```bash -officecli set doc.docx "/header[1]" --prop text="New Company Name" --prop font=Calibri --prop size=9pt -officecli set doc.docx "/footer[1]" --prop text="Confidential - Page " -``` - -### Update Charts - -```bash -# Update chart data (existing series only) -officecli set doc.docx "/chart[1]" --prop data="2025:51,67,74,92" - -# Update chart title -officecli set doc.docx "/chart[1]" --prop title="Updated Revenue Trend" - -# Update categories -officecli set doc.docx "/chart[1]" --prop categories="Q1,Q2,Q3,Q4" - -# Delete and recreate chart (to change series count) -officecli remove doc.docx "/body/chart[1]" -officecli add doc.docx /body --type chart --prop chartType=column --prop categories="Q1,Q2,Q3,Q4" --prop series1="Rev:51,67,74,92" --prop series2="Cost:30,35,38,42" --prop width=15cm --prop height=10cm --prop colors=1F4E79,4472C4 -``` - -**WARNING**: `set --prop data=` can only update existing series -- it cannot add new series. If you need more series than the chart currently has, delete and recreate. - -### Find/Replace - -```bash -# Global find/replace -officecli set doc.docx / --prop find="2024" --prop replace="2025" - -# Scoped find/replace -officecli set doc.docx / --prop find="Acme Inc" --prop replace="Acme Corporation" --prop scope=all - -# Body only (skip headers/footers) -officecli set doc.docx / --prop find="old term" --prop replace="new term" --prop scope=body - -# Headers/footers only -officecli set doc.docx / --prop find="Company Name" --prop replace="Acme Corp" --prop scope=headers -``` - -**WARNING: Find/replace performs substring matching, not whole-word matching. Replacing "ACME" in "ACME Corporation" produces "New Name Corporation". After any find/replace, review with `view text` and run a second cleanup pass if needed.** - -### Accept/Reject Tracked Changes - -```bash -officecli set doc.docx / --prop accept-changes=all -officecli set doc.docx / --prop reject-changes=all -``` - -**Note**: Only `all` is supported for accept/reject scope. Selective acceptance by author or range is not available via high-level commands. - -### Update Metadata - -```bash -officecli set doc.docx / --prop title="Updated Report Title" --prop author="New Author" --prop lastModifiedBy="Editor" -``` - ---- - -## Template Editing Pitfalls - -### Pitfall: Style Drift - -When editing a document with established styles, always check what styles exist before adding content: - -```bash -officecli view doc.docx stats -officecli get doc.docx /styles -``` - -Use existing style names rather than inline formatting to maintain consistency. If the document uses `Heading1` with Georgia 16pt, apply `--prop style=Heading1` instead of manually setting font/size. - -### Pitfall: Index Shifting After Structural Changes - -**CRITICAL**: When you remove `/body/p[5]`, what was `p[6]` becomes `p[5]`. Always: -- Remove from highest index to lowest -- Complete all structural changes before content edits -- Re-query with `get /body --depth 1` after structural changes - -### Pitfall: Overwriting Headers/Footers - -Setting a new header replaces the existing one. If the document has different first-page and default headers, setting the default header does not affect the first-page header (and vice versa). Check which header types exist: - -```bash -officecli get doc.docx "/header[1]" -``` - -### Pitfall: Row-Level Formatting - -Row-level `set` does NOT support `bold`, `shd`, `color`, or `font`. These properties are silently ignored or error. All formatting must go through cell-level paths: - -```bash -# WRONG -- bold/shd/color ignored at row level -officecli set doc.docx "/body/tbl[1]/tr[1]" --prop c1="Header" --prop bold=true --prop shd=1F4E79 - -# CORRECT -- text at row level, formatting at cell level -officecli set doc.docx "/body/tbl[1]/tr[1]" --prop c1="Header" -officecli set doc.docx "/body/tbl[1]/tr[1]/tc[1]" --prop bold=true --prop shd=1F4E79 --prop color=FFFFFF -``` - ---- - -## Bulk Modifications with Query - -```bash -# Find all Heading1 paragraphs -officecli query doc.docx 'paragraph[style=Heading1]' - -# Batch-set font on all Heading1 paragraphs -officecli set doc.docx 'paragraph[style=Heading1]' --prop font=Georgia --prop color=1F4E79 - -# Find and review empty paragraphs -officecli query doc.docx 'p:empty' - -# Set style on all paragraphs matching text -officecli set doc.docx 'p:contains("important")' --prop bold=true -``` - ---- - -## Raw XML Escape Hatch - -When the high-level CLI cannot express what you need, fall back to raw XML. See [creating.md](creating.md#l1-l2-l3-escalation-when-to-use-raw-xml) for the full L1/L2/L3 escalation guide. - -```bash -# View raw XML -officecli raw doc.docx /document - -# Modify raw XML -officecli raw-set doc.docx /document --xpath "//w:body/w:p[1]/w:pPr/w:jc" --action setattr --xml "w:val=center" - -# Add tab stops (not available via high-level commands) -officecli raw-set doc.docx /document --xpath "//w:body/w:p[1]/w:pPr" --action append --xml '' -``` - -**Use raw XML only as a last resort.** The high-level CLI handles most operations correctly without XML knowledge. - -**NOTE: Comments can only be attached to paragraphs (`/body/p[N]`) or runs (`/body/p[N]/r[M]`), not to table cells directly. To comment on a table cell value, add the comment to a nearby paragraph instead.** diff --git a/skills/officecli-financial-model/SKILL.md b/skills/officecli-financial-model/SKILL.md index ee9d01777..8dbd01d70 100644 --- a/skills/officecli-financial-model/SKILL.md +++ b/skills/officecli-financial-model/SKILL.md @@ -1,182 +1,561 @@ --- -# officecli: v1.0.24 name: officecli-financial-model -description: "Use this skill when the user wants to build a financial model, - 3-statement model, DCF valuation, cap table, scenario analysis, or - financial projections in Excel. Trigger on: 'financial model', - '3-statement model', 'DCF', 'cap table', 'pro forma', 'projections', - 'sensitivity analysis', 'waterfall', 'debt schedule', 'break-even', - 'discounted cash flow', 'capitalization table', 'fundraising model', - 'WACC calculation', 'scenario analysis model'. - Input is a text prompt with assumptions. Output is a single .xlsx file - with formula-driven, interconnected statement sheets." +description: "Use this skill when the user wants to build a financial model — 3-statement model, DCF valuation, LBO, SaaS unit economics, sensitivity / scenario analysis, debt schedule, or fundraising projections — in Excel. Trigger on: 'financial model', '3-statement model', 'P&L + BS + CF', 'DCF', 'WACC', 'NPV', 'terminal value', 'LBO', 'debt schedule', 'cash sweep', 'MOIC', 'IRR / XIRR', 'sensitivity table', 'scenario analysis', 'ARR model', 'unit economics', 'CAC / LTV', 'cap table forecast'. Output is a single formula-driven .xlsx. This skill is a scene layer on top of officecli-xlsx — it inherits every xlsx v2 rule (4-color code, visual floor, number formats, cache-drift, Known Issues, Delivery Gate minimum cycle). DO NOT invoke for a simple budget tracker, CSV dump, or operational KPI sheet — route those to officecli-xlsx base." --- -# Financial Model Skill +# OfficeCLI Financial-Model Skill -Build formula-driven, multi-sheet financial models from scratch in Excel. Every number on every statement sheet is a formula referencing the Assumptions sheet. Output is a single `.xlsx` file with interconnected sheets -- Income Statement, Balance Sheet, Cash Flow Statement, and optional valuation or scenario analysis sheets. +**This skill is a scene layer on top of `officecli-xlsx`.** Every xlsx hard rule — shell quoting, incremental execution, Help-First Rule, visual delivery floor, CFO 4-color code (blue input / black formula / green cross-sheet / yellow-fill assumption), number-format standards (years as text, zero as `-`, `%` one decimal, negatives in parens), assumption-cell discipline, CSV batch import, chart data-feed forms (a/b/c), the 5-gate Delivery cycle, cache-drift guidance, Known Issues (the cross-sheet `!` trap, batch + resident for formulas, renderer caveats) — is **inherited, not re-taught**. This file adds only what a **financial model** requires on top: three-zone architecture, 3 model-type recipes (3-statement / DCF / LBO), sensitivity + scenario protocols, financial-function patterns, circular-reference discipline, and model-specific Delivery Gates 4–6. ---- +When the xlsx base rules cover it, the text here says `→ see xlsx v2 §X`. Read `skills/officecli-xlsx/SKILL.md` first if you have not. + +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` + +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. + +## Help-First Rule + +This skill teaches what a financial model requires, not every CLI flag. When a prop name / alias / enum is uncertain, consult help BEFORE guessing: `officecli help xlsx [element] [--json]`. Help is pinned to installed version — when this skill and help disagree, **help wins**. Every `--prop X=` below was verified against `officecli help xlsx ` on v1.0.63. + +## Mental Model & Inheritance + +**Inherits xlsx v2.** Read `skills/officecli-xlsx/SKILL.md` first. This skill assumes you know `create` / `open` / `close`, `set` values/formulas, `batch` heredocs for cross-sheet formulas, `/SheetName/A1` paths, named ranges, the 5-gate Delivery cycle, the cross-sheet `!` trap, and that **cross-sheet formulas go non-resident (single batch OR individual `set`), never batch-while-resident**. + +## Shell & Execution Discipline + +Shell quoting, incremental execution, `$FILE` convention → see xlsx v2 §Shell & Execution Discipline. Same rules: quote every `[N]` path, single-quote any prop containing `$` (every number format here — `$#,##0;($#,##0);"-"` — needs single quotes), no hand-written `\$`/`\t`/`\n`, one command at a time. Examples below use `$FILE` (`FILE="model.xlsx"`). -## BEFORE YOU START (CRITICAL) +## Core Principles (identity) -**Every time before using officecli, run this check:** +A financial model is an xlsx with a **decision-grade, formula-driven layer**: every output traces an unbroken chain to blue-font assumptions, every statement balances every period, every valuation is re-auditable. Eight deltas on top of a general xlsx: + +1. **Three-zone architecture mandatory:** Inputs → Calc → Outputs. Collapsing zones → unauditable. +2. **Assumptions live in cells, never inside formulas.** `=B5*(1+Assumptions!GrowthRate)`, never `=B5*1.05`. +3. **Statements balance every period.** `Assets − Liab − Equity = 0`, `CF.EndingCash = BS.Cash`. Gate 4 fails on `IMBALANCED`. +4. **Hardcodes audited.** Calc sheets carry zero hardcoded numbers; Gate 6 counts. +5. **Sensitivity / scenario is first-class.** 2-axis grid, dropdown `INDEX/MATCH` switch, or Base/Upside/Downside cols. Excel Data Tables not reliably supported — manual grids only. +6. **Cached values on valuation cells load-bearing.** NPV / IRR / XNPV caching `0` ships a wrong number to non-recalculating readers. Gate 5 spot-checks. +7. **Circularity is a design choice.** Legitimate rings (interest ↔ cash, revolver plug ↔ ending cash) use `calc.iterate=true`. Accidental circularity is broken algebra — never papered with `iterate`. +8. **Named ranges for ≥ 3-use assumptions.** `WACC`, `TaxRate`, `TerminalGrowth`, `ExitMultiple`, `ChurnRate`. Declared-unused names are dead decoration — Gate 6 flags. + +### Reverse handoff — when to go BACK to xlsx base + +Stay in **xlsx base** for: budget trackers, CSV-to-report dumps, operational KPI sheets, simple templates, cap tables without forecast logic. Use **this skill** only when the ask mentions: 3-statement / DCF / WACC / NPV / TV / LBO / debt schedule / MOIC / IRR / unit economics / ARR roll-forward / sensitivity grid / scenario switch / pro forma. + +## Three-zone architecture (hard rule) + +Every model in this skill builds on three zones. **Name them, tab-color them, and enforce them with executable audits.** Breaking the zone rule is the single most common cause of an unauditable model. + +| Zone | Sheet names (convention) | Tab color | Content | Hardcodes | Formulas | +|---|---|---|---|---|---| +| **Inputs** | `Assumptions`, `Inputs`, `Drivers` | Yellow `FFC000` | Raw drivers: growth rates, margins, tax, WACC, FTE, pricing, working-capital days | Blue `0000FF` on every cell | Allowed only for derived assumptions (e.g. `=MonthlyARPU*12`) | +| **Calc** | `P&L`, `Balance Sheet`, `Cash Flow`, `DCF`, `Debt`, `ARR` | Blue `4472C4` | All derivations and statements | **Zero** (enforced by Gate 6) | Black `000000` for same-sheet, green `008000` for cross-sheet | +| **Outputs** | `Summary`, `Dashboard`, `Sensitivity`, `Returns` | Green `70AD47` | KPIs, sensitivity grids, charts, returns waterfall | Only for labels (non-numeric); Gate 6 counts numeric hardcodes → 0 | Black / green per above | + +**Build order is cross-zone-aware.** Assumptions first, then Calc bottom-up on the dependency chain (`IS → BS → CF` for 3-statement; `FCF → WACC → NPV` for DCF), then Outputs last. Building Outputs first caches `0` everywhere and downstream inherits zeros. + +**Executable zone audit** (run before Gate 4): ```bash -if ! command -v officecli &> /dev/null; then - echo "Installing officecli..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - # Windows: irm https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.ps1 | iex -else - CURRENT=$(officecli --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - LATEST=$(curl -fsSL https://api.github.com/repos/iOfficeAI/OfficeCLI/releases/latest | grep '"tag_name"' | sed -E 's/.*"v?([0-9.]+)".*/\1/') - if [ "$CURRENT" != "$LATEST" ]; then - echo "Upgrading officecli $CURRENT -> $LATEST..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - else - echo "officecli $CURRENT is up to date" - fi -fi -officecli --version +# Calc zone: zero numeric hardcodes allowed. NOTE: `:not(:has(formula))` pseudo doesn't filter on v1.0.63+ — filter via jq on .format.formula == null. +HARDCODE=$(officecli query "$FILE" 'cell[type=Number]' --json | jq '[.data.results[] | select(.format.formula == null) | select(.path | test("/(P&L|Balance Sheet|Cash Flow|DCF|Debt|ARR)/"))] | length') +[ "$HARDCODE" -eq 0 ] && echo "Zone audit OK" || { echo "REJECT: $HARDCODE hardcoded numeric cells on Calc sheets — move to Assumptions"; exit 1; } +# Assumptions zone: should be non-zero. +INPUTS=$(officecli query "$FILE" '/Assumptions/cell[type=Number]' --json | jq '[.data.results[] | select(.format.formula == null)] | length') +[ "$INPUTS" -ge 5 ] && echo "Assumptions has $INPUTS hardcoded drivers" || echo "WARN: Assumptions has only $INPUTS inputs" ``` ---- +## Print delivery (board / IC / LP) -## Use When +When the ask contains "print" / "一页" / "董事会" / "投资人" / "IC memo" / "LP update", the print pipeline must emit **only** the Outputs zone. Two artefacts: -- User provides assumptions and asks for financial projections or a financial model -- User asks for an "income statement", "balance sheet", or "cash flow statement" -- User asks for DCF valuation, WACC calculation, or discounted cash flow analysis -- User asks for a cap table, waterfall analysis, or fundraising model -- User asks for scenario analysis, sensitivity table, debt schedule, or break-even model +```bash +# 1. Print_Area scoped to the Outputs sheet (Summary or Dashboard). +officecli add "$FILE" / --type namedrange --prop name=_xlnm.Print_Area --prop scope=Summary --prop 'refersTo=Summary!$A$1:$H$40' +# 2. Hide every non-Outputs sheet — Print_Area scope alone does NOT stop the print pipeline from emitting every visible sheet. +for S in Assumptions 'P&L' 'Balance Sheet' 'Cash Flow' DCF WACC Debt FCF 'S&U' Exit Returns; do + officecli raw-set "$FILE" /workbook --xpath "//x:sheet[@name='$S']" --action setattr --xml "state=hidden" || true +done +# 3. fit-to-page landscape on Outputs sheet. +officecli raw-set "$FILE" /Summary --xpath "//x:worksheet" --action prepend --xml '' +``` -## Do NOT Use When +Delete any `Print_Area` set on Calc sheets — conflicting scopes emit multi-page output with Assumptions / statement sheets leaking. -| User Request | Correct Skill | -|-------------|--------------| -| CSV data to dashboard / charts | officecli-data-dashboard | -| Edit/modify an existing .xlsx | officecli-xlsx (editing.md) | -| KPI dashboard or metrics summary | officecli-data-dashboard | -| 1-2 sheet visualization from existing data | officecli-data-dashboard | -| Word document or PowerPoint | officecli-docx / officecli-pitch-deck | +## Build-order & cache-drift rule (critical for 3-statement) ---- +Three facts cause silent wrong numbers: (1) new formulas ship without cached values — Excel recomputes on open, HTML preview / older viewers do not; (2) downstream written in the same sequence as upstream caches `0` from upstream's pre-cache state; (3) cross-sheet `batch` while resident is open deadlocks at 3–5 ops. -## What This Skill Produces +**Discipline (every recipe):** +- Build order follows the data chain: `P&L → BS → CF` (3-statement); `FCF → WACC → NPV → Sensitivity` (DCF); `S&U → Debt → P&L → CF → Returns` (LBO). +- After the cross-sheet chain, **cache-refresh pass:** re-issue `set` on every summary / valuation / balance-check cell, non-resident. +- Spot-check: `officecli get "$FILE" /Summary/B2 --json | jq .format.cachedValue` returns non-zero non-null. `null` ≠ `0`: `null` means Excel will compute on open (OK for delivery); `0` is a cached lie. If `0` persists: close residents, re-set; still `0` → cache-fallback (§Financial function patterns). -A single `.xlsx` file with 4-10 interconnected sheets: +## Recipes — three model types -| Sheet Type | Purpose | Key Characteristic | -|------------|---------|-------------------| -| Assumptions | All hardcoded inputs in one place | Blue font (`0000FF`) on every input cell | -| Income Statement | Revenue through Net Income | All rows are formulas referencing Assumptions | -| Balance Sheet | Assets, Liabilities, Equity | Must balance every period; includes check row | -| Cash Flow Statement | Operating, Investing, Financing | Ending Cash must equal BS Cash | -| DCF / Valuation | WACC, FCF, Terminal Value, Equity Value | Named ranges for key inputs | -| Sensitivity Table | 2-variable grid of implied values | Each cell is a self-contained formula | -| Scenarios | Dropdown-driven Base/Bull/Bear | IF/INDEX formulas reference dropdown | -| Error Checks | Balance, cash reconciliation, ISERROR scan | "ALL CLEAR" or "ERRORS FOUND" summary | -| Dashboard / Charts | Visual summary of model outputs | Charts use cell range references | +Each recipe below is **runnable skeleton, not finance theory**. Substitute numbers; don't restructure. All recipes assume `FILE="model.xlsx"` is set and you have run `officecli create "$FILE"` + `officecli open "$FILE"`. Close with `officecli close "$FILE"` at the end. -ALL values on statement sheets are formulas. The only hardcoded numbers are on the Assumptions sheet. +### Recipe A — 3-statement model (P&L + BS + CF) ---- +**What this recipe produces.** 4 sheets: `Assumptions`, `P&L`, `Balance Sheet`, `Cash Flow`, plus `Summary`. Year columns 2024A · 2025E · 2026E · 2027E. Balance-check row on BS; cash-reconciliation row on CF. Every statement row = formula → Assumptions. -## Core Concepts +**Build order (MANDATORY).** `Assumptions → P&L → Balance Sheet → Cash Flow → Summary`. Do NOT build BS before P&L — `RetainedEarnings` depends on `NI`. Do NOT build CF before BS — `CF.OpeningCash = prior period CF.EndingCash` self-chain requires BS cash anchored for Y1. The skill's Gate 4 balance check fails silently if order is wrong. -- **Assumptions-First Architecture** -- ALL hardcoded inputs go on the Assumptions sheet. Every other sheet references Assumptions. Changing one assumption recalculates the entire model. -- **Financial Color Coding** -- `font.color=0000FF` (blue) for inputs, `font.color=000000` (black) for formulas, `font.color=008000` (green) for cross-sheet references. Non-negotiable convention. -- **Formula Chain Integrity** -- Every derived value traces back to the Assumptions sheet through an unbroken chain of formula references. -- **Error Checking** -- Balance checks (Assets = Liabilities + Equity), cash reconciliation (CF ending cash = BS cash), and ISERROR scans on every sheet. -- **Batch-First Workflow** -- Use heredoc batch for ALL multi-cell operations, especially cross-sheet formulas. Verify after each batch. +**Step 1 — sheets + tab colors + freeze panes.** ---- +```bash +officecli add "$FILE" / --type sheet --prop name=Assumptions --prop tabColor=FFC000 +officecli add "$FILE" / --type sheet --prop name=P&L --prop tabColor=4472C4 +officecli add "$FILE" / --type sheet --prop name='Balance Sheet' --prop tabColor=4472C4 +officecli add "$FILE" / --type sheet --prop name='Cash Flow' --prop tabColor=4472C4 +officecli add "$FILE" / --type sheet --prop name=Summary --prop tabColor=70AD47 +officecli set "$FILE" /Assumptions --prop freeze=B2 +officecli set "$FILE" /P&L --prop freeze=B3 +officecli set "$FILE" "/Balance Sheet" --prop freeze=B3 +officecli set "$FILE" "/Cash Flow" --prop freeze=B3 +``` -## Workflow Overview +**Step 2 — assumptions (blue, yellow-fill on key drivers).** Year headers row 2, labels down col A, blue numeric inputs on B:E. Drivers: `RevenueGrowth`, `GrossMargin`, `OpExRatio`, `TaxRate`, `DaysReceivable/Inventory/Payable`, `CapExRatio`, `DepreciationYears`. `font.color=0000FF` on B:E. Yellow-fill (`fill=FFFF00`) the 3–5 scenario-switched drivers. -**Phase 1: Understand** -- Identify model type (3-statement, DCF, cap table, scenario). Determine which sheets are needed and the formula dependency chain. +**Declare named ranges for ≥3-use drivers and reference them** (`StartingARR`, `TaxRate`, `OpeningCash`, `GrowthRate`, `GrossMargin`). Formulas: `=StartingARR` not `=Assumptions!B4`; `=EBT*TaxRate` not `=EBT*Assumptions!B8`. Declared-unused names = dead decoration, Gate 6 rejects. -**Phase 2: Plan** -- Map sheet structure, column layout (labels + year columns), and cross-sheet formula dependencies. Define the build order. +**Step 3 — P&L rows (all formulas).** Rows: `Revenue` / `COGS` / `Gross Profit` / `OpEx` / `EBITDA` / `D&A` / `EBIT` / `Interest` / `EBT` / `Tax` / `Net Income`. Every row = formula referencing `Assumptions` or prior-row cells. Example revenue-side block — **substitute your row numbers**. Row-map for this example: `B3=Revenue, B4=COGS, B5=Gross Profit, B7=OpEx, B9=EBITDA, B10=EBIT, B15=Net Income`. Submit as single non-resident batch: -**Phase 3: Build** -- Follow the mandatory 10-step build sequence in creating.md Section A.7. Build in layers: structure, assumptions, formulas (IS then BS then CF), formatting, charts, protection, raw-set, validate. +```bash +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/P&L/B3","props":{"formula":"Assumptions!B5","font.color":"008000"}}, + {"command":"set","path":"/P&L/C3","props":{"formula":"B3*(1+Assumptions!C6)"}}, + {"command":"set","path":"/P&L/D3","props":{"formula":"C3*(1+Assumptions!D6)"}}, + {"command":"set","path":"/P&L/E3","props":{"formula":"D3*(1+Assumptions!E6)"}}, + {"command":"set","path":"/P&L/B4","props":{"formula":"-B3*(1-Assumptions!B7)"}}, + {"command":"set","path":"/P&L/B5","props":{"formula":"B3+B4"}} +] +EOF +``` -**Phase 4: QA** -- Run the QA checklist: validate, formula error scan, cross-sheet verification, balance check, cash reconciliation, chart data check. +Assumptions refs (`B5`, `C6`, `B7`) are also placeholder rows — better: **define named ranges** for each driver (Step 2) so formulas read `=StartingRevenue*(1+RevenueGrowth_Y2)` regardless of row layout. Repeat for `OpEx` / `D&A` / `Interest` / `Tax` / `NI`. `font.color=008000` on every cross-sheet-reference cell; same-sheet cells default `000000`. `numFmt='$#,##0;($#,##0);"-"'` on all $ rows. -**Phase 5: Deliver** -- Deliver the `.xlsx` file. Note that formulas recalculate on open (fullCalcOnLoad is set). +**Step 4 — Balance Sheet rows (all formulas).** Assets = `Cash + AR + Inventory + Net PP&E`. Liab = `AP + Debt`. Equity = `OpeningEquity + RetainedEarnings`. Working-capital rows use Days assumptions: `AR = Revenue × DaysReceivable / 365`. `Net PP&E` rolls forward: `Beg + CapEx − Depreciation`. **`BS.Cash` is NOT an independent plug** — it MUST equal `'Cash Flow'!B` (populated in Step 5). ---- +**Retained Earnings — live formula every period.** `BS.RE(t) = BS.RE(t-1) + 'P&L'!NI(t) − Dividends(t)`. Hardcoded RE rounds to whole dollar → BS shows ±$1 off every period (CFO reads "model doesn't balance"). For Y1 Historical RE (no prior NI), compute via BS identity as a **live formula**: `BS!RE_Y1 = TotalAssets − TotalLiabilities − PaidInCapital`. Blue-font + classic comment on the Y1 cell; Y2..Y5 stay NI-driven. -## Quick Reference: Key Warnings - -| Warning | Detail | -|---------|--------| -| **Sheet names with spaces in CLI paths (REQUIRED)** | When using `officecli get` or `officecli set` with a sheet name that contains spaces (e.g., "Cash Flow", "ARR Waterfall"), the sheet name portion of the path MUST be wrapped in single quotes: `officecli get model.xlsx "'/Cash Flow/A1'"`. Without single quotes, the command fails with Err:509. In JSON batch paths, no extra quoting is needed (the JSON string already handles it). | -| Cross-sheet `!` escaping | Use heredoc batch for ALL cross-sheet formulas. Verify with `officecli get` after each batch. | -| Batch size limit | 15-20 ops per batch in resident mode (recommended). 8-12 ops without resident mode. | -| Batch JSON values | ALL values must be strings: `"true"` not `true`, `"24"` not `24` | -| fullCalcOnLoad + iterate | MANDATORY. Use `officecli set model.xlsx / --prop calc.fullCalcOnLoad=true --prop calc.iterate=true`. Do NOT use raw-set (creates duplicates). | -| Blue inputs / black formulas | `font.color=0000FF` on Assumptions inputs, `font.color=000000` on all formula cells | -| **Header row background color (REQUIRED)** | All section header rows MUST have `fill` set. Use `"fill":"1F3864","font.color":"FFFFFF"` (dark navy + white text). Header rows without fill fail the Quality Bar check (Q2). | -| **Header row fill MUST cover ALL columns (A:D or A:G)** | Section header rows (REVENUE, ASSETS, etc.) must apply the dark fill to ALL columns in that row, not just the A-column label cell. If only A is filled, B/C/D columns on that row show black text on a white background — extremely poor readability against the dark header. Correct: `{"command":"set","path":"/P&L/A5:D5","props":{"shd":"1F3864","color":"FFFFFF","bold":true}}`. Wrong: set only `/P&L/A5`. | -| Balance sheet must balance | Explicit check formula: `=TotalAssets - TotalLiabilities - TotalEquity` must equal 0 | -| Cash reconciliation | CF ending cash must equal BS cash for every period | -| No Excel Data Tables | Sensitivity tables must be manual formula grids. Each cell is an explicit self-contained formula. | -| Number format `$` quoting | Use heredoc batch or single quotes to prevent shell expansion of `$` | -| Named ranges required | Define for all key assumptions (WACC, growth rates, tax rate). Required for auditability. | -| Column widths | No auto-fit. Set explicitly: labels=22-28, numbers=14-18, year headers=12-14 | -| formulacf no font.bold | Use `fill` + `font.color` only. `font.bold` causes validation errors. | -| raw-set ordering | activeTab raw-set MUST be the last command. calcPr uses high-level `set / --prop calc.*` instead of raw-set. | -| BS Cash = CF Ending Cash | BS Cash ALWAYS equals `=Cash Flow!B19`, including Year 1. Never use cash-as-plug or reference Assumptions directly. | -| Chart title `$` in shell | Use heredoc batch for chart titles containing `$` to prevent shell expansion. | -| **Freeze panes (REQUIRED on every data sheet)** | Every sheet with column headers must have freeze panes set (`freeze=B2` or `freeze=B3`). Missing freeze panes is a Hard Rule violation. Apply in Step 8e of build sequence. | -| **Formula cells black font (REQUIRED)** | All formula cells on statement sheets must have `font.color=000000`. Omitting this makes the Blue=Input / Black=Formula color convention invisible. Apply in Step 8f of build sequence. **Step 8f applies to EVERY formula cell on EVERY statement sheet** — Income Statement, Balance Sheet, Cash Flow, Assumptions (roll-forward/output rows), and all auxiliary sheets. Apply by row range for each sheet; do NOT apply to only one sheet or one section. | -| **H8: Summary or Dashboard sheet (REQUIRED)** | Every financial model output MUST include a Summary or Dashboard sheet containing at least 4 KPI values sourced from the statement sheets (e.g., Revenue, EBITDA, Net Income, Ending Cash, or equivalent). A model delivered without a Summary/Dashboard sheet fails this Hard Rule. Specialized single-purpose models (ARR Waterfall, DCF-only) satisfy this with a dedicated Summary tab. Tab color: `70AD47` (green). | +**Step 5 — Cash Flow rows (all formulas).** Operating: `NI + D&A − ΔWorkingCapital`. Investing: `−CapEx`. Financing: `ΔDebt − Dividends`. Ending Cash = `Opening + Operating + Investing + Financing`. **Year 2+ Opening Cash = prior period Ending Cash** — self-chain on the same sheet: `C17 = B19`, `D17 = C19`, `E17 = D19`. The Y1 `OpeningCash` is an Assumptions input. ---- +**Step 6 — Balance check + cash reconciliation rows (hard delivery checks).** Row-map for this example: `Balance Sheet: B10=Total Assets, B15=Total Liab, B17=Total Equity, B18=Balance Check`; `Cash Flow: B5=BS.Cash (cross-sheet anchor), B19=CF.Ending Cash, B21=CF-BS Cash Recon`. Substitute your layout's rows — the logic is the check, not the cell addresses. -## Pre-Delivery Checklist +```bash +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/Balance Sheet/B18","props":{"formula":"IF(ABS(B10-B15-B17)<0.01,\"OK\",\"IMBALANCED: \"&ROUND(B10-B15-B17,0))","bold":"true","font.color":"000000"}}, + {"command":"set","path":"/Cash Flow/B21","props":{"formula":"IF(ABS(B19-'Balance Sheet'!B5)<0.01,\"OK\",\"CF != BS CASH: \"&ROUND(B19-'Balance Sheet'!B5,0))","bold":"true"}} +] +EOF +``` -Before delivering the `.xlsx` file, verify all items: +Replicate across columns C/D/E. Apply red fill (`fill=FFC7CE`) conditionally via `type=containsText --prop text=IMBALANCED` or `text="CF !="`. Gate 4 queries these rows and refuses delivery on any `IMBALANCED`. -| # | Check | How to Verify | -|---|-------|---------------| -| 1 | Freeze panes on every data sheet | `officecli get model.xlsx "/Sheet"` — confirm `freeze` property is set | -| 2 | Formula cells have black font (`000000`) | **No CLI command can reliably detect font color.** `officecli view text` does NOT output font color information — any grep-based check against color values is a false negative and always returns 0 lines regardless of actual color. **Must verify visually:** run `officecli view model.xlsx html > preview.html` and open in browser, or take a screenshot, and confirm formula cells appear in black. Apply Step 8f to ALL sheets before verifying. | -| 3 | Input cells have blue font (`0000FF`) | Assumptions sheet cells B:D must be blue | -| 4 | Header rows fill covers ALL columns (A:D or wider) | Screenshot check — no white-background cells in header rows | -| 5 | Balance Check = TRUE every period | `officecli get model.xlsx "/Balance Sheet/B18" --json` | -| 6 | Cash Reconciliation = TRUE every period | `officecli get model.xlsx "/Cash Flow/B21" --json` | -| 7 | No `\!` in cross-sheet formulas | `officecli get model.xlsx "/Income Statement/B3"` — no backslash | -| 8 | fullCalcOnLoad + iterate set | `officecli validate model.xlsx` passes | -| 9 | Year 2+ Beginning ARR = Prior Year Ending ARR | No independent "Opening ARR" input in Assumptions for Year 2+ | -| 10 | Opening Equity = Opening Assets - Opening Liabilities | Balance Check Year 1 = TRUE (not hardcoded equity value) | +**Step 7 — cache refresh + format pass.** Re-set every summary cell on `Summary`, every balance-check / recon cell, and every cross-sheet reference on BS / CF (non-resident, single batch per sheet). Apply column widths (`col[A]=28`, `col[B:E]=15`), `numberformat='$#,##0;($#,##0);"-"'` on all dollar rows, header fills (`fill=1F3864`, `font.color=FFFFFF`, `bold=true`) on section-header rows (REVENUE / COGS / ASSETS / LIABILITIES). Header fill must cover A:E, not just the label cell (→ xlsx v2 §visual floor). ---- +**Step 8 — Summary / Dashboard KPIs + charts.** Minimum 4 KPIs: `Revenue 27E`, `EBITDA Margin 27E`, `Ending Cash 27E`, `Net Income CAGR` — each a formula referencing a statement cell, green font. -## Known Issues - -| Issue | Workaround | -|-------|------------| -| `!` escaping in cross-sheet formulas | Always use heredoc batch. Verify with `officecli get`. | -| Batch failure at scale | Use resident mode + 15-20 ops per batch. If failure persists, reduce to 8-12. Retry individually as last resort. | -| Cannot rename sheets | Plan sheet names upfront before creation. | -| Sensitivity tables are manual | Each cell needs an explicit formula. No Excel DATA TABLE support. | -| Chart series fixed at creation | Cannot add series later. Plan all series before `add`. | -| Formula cached values blank | `view text` shows blank for formula cells. This is normal. Set fullCalcOnLoad. | -| Waterfall chart totals | Cannot mark bars as totals programmatically. Use color convention. | -| Circular references | Use ``. Design model to avoid unnecessary circularity. | -| Chart title `$` stripping | Shell expands `$` in `--prop title`. Use heredoc batch for chart titles with `$`, or omit `$` from titles. | -| AP formula sign error | AP (Accounts Payable) must be a positive number. If COGS is stored as a negative value, AP formula must negate it: `=-COGS*DPO/365`. Wrong sign causes NWC overstatement and incorrect Cash Flow direction. See creating.md C.6 WARNING. | -| `#NAME?` not detectable by query/validate | `officecli query` and `validate` cannot detect runtime `#NAME?` errors from unquoted sheet names (e.g., `P&L!B3`). Run screenshot check (creating.md E.8 Step 10) when any sheet name contains `&`, spaces, or parentheses. | +**Minimum 3 charts on any Dashboard delivered to a board / executive audience** — one chart is a draft, three is a deliverable. Pre-populate `Summary!A10:E13` with Gross Margin / EBITDA Margin / NI Margin ratio rows (formulas referencing `P&L`) before adding the margin chart. ---- +```bash +# (1) Top-line trend (Revenue + EBITDA). +officecli add "$FILE" /Summary --type chart --prop chartType=column --prop dataRange='P&L!A2:E5' --prop title='Revenue & EBITDA' --prop width=14cm --prop height=8cm +# (2) Margin trend (Gross / EBITDA / NI margin). +officecli add "$FILE" /Summary --type chart --prop chartType=line --prop dataRange='Summary!A10:E13' --prop title='Margin trend' --prop width=14cm --prop height=8cm +# (3) Cash trajectory (Ending Cash ± Runway). +officecli add "$FILE" /Summary --type chart --prop chartType=area --prop dataRange='Cash Flow!A19:E19' --prop title='Ending cash' --prop width=14cm --prop height=8cm +``` + +**Verification (run all three):** + +```bash +# Balance check every period must say OK +officecli get "$FILE" "/Balance Sheet/B18:E18" --json | jq '.data[].cachedValue // .data[].value' +# Cash recon every period must say OK +officecli get "$FILE" "/Cash Flow/B21:E21" --json | jq '.data[].cachedValue // .data[].value' +# Summary KPIs are plausible numbers, not 0 or null +officecli get "$FILE" "/Summary/B2:B5" --json | jq '.data[].cachedValue' +``` + +### Recipe B — DCF valuation + +**What this recipe produces.** Sheets: `Assumptions`, `FCF` (10-year forecast), `WACC` (panel), `DCF` (NPV + TV + equity bridge), `Sensitivity` (2-axis grid). Output: `Implied Equity Value` + `Implied Per-Share`, with a `WACC × g` sensitivity. + +**Build order.** `Assumptions → FCF → WACC → DCF → Sensitivity`. + +**Step 1 — named ranges for key drivers.** DCF's readability depends on names. Every formula below uses `WACC`, `TaxRate`, `g` — not `$B$6`: + +```bash +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"add","parent":"/","type":"namedrange","props":{"name":"WACC","ref":"WACC!$B$12"}}, + {"command":"add","parent":"/","type":"namedrange","props":{"name":"TaxRate","ref":"Assumptions!$B$8"}}, + {"command":"add","parent":"/","type":"namedrange","props":{"name":"TerminalGrowth","ref":"Assumptions!$B$15"}}, + {"command":"add","parent":"/","type":"namedrange","props":{"name":"NetDebt","ref":"Assumptions!$B$20"}}, + {"command":"add","parent":"/","type":"namedrange","props":{"name":"SharesOut","ref":"Assumptions!$B$21"}} +] +EOF +``` + +**Step 2 — FCF build (10 years).** Columns B:K = Y1..Y10. Rows: `Revenue` (from growth) / `EBIT` (revenue × margin) / `EBIT × (1 − TaxRate)` (NOPAT) / `+ D&A` / `− CapEx` / `− ΔNWC` / `= FCF`. Use Assumptions-driven ratios (`CapEx = Revenue × CapExRatio`). All cells formulas, black font, `numFmt='$#,##0;($#,##0);"-"'`. + +**Step 3 — WACC panel.** On `WACC` sheet, an 8-row panel: `Risk-free rate` / `Equity risk premium` / `Beta` / `Cost of equity` (=Rf + β×ERP) / `Pre-tax debt cost` / `After-tax debt cost` (=×(1−TaxRate)) / `Equity weight` / `Debt weight` / `WACC` (=We×Re + Wd×Rd_after_tax). Inputs blue; derived rows black. + +**Step 4 — Terminal value + NPV + equity bridge.** Row-map: `DCF: B/C 3=TV, 4=PV explicit FCF, 5=PV terminal, 6=EV, 7=Net Debt, 8=Equity Value, 9=Per-Share`; `FCF: row 2 = periods (1..10), row 11 = FCF, B:K = Y1..Y10`. Substitute your rows. Notes column cells use `{"value":"text"}`, never `{"formula":"..."}` — formula-style prose yields `#NAME?` on open (see callout after Recipe C Step 5). On `DCF` sheet: + +```bash +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/DCF/B3","props":{"value":"Terminal value (Gordon growth)"}}, + {"command":"set","path":"/DCF/C3","props":{"formula":"FCF!K11*(1+TerminalGrowth)/(WACC-TerminalGrowth)","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B4","props":{"value":"PV of explicit-period FCF (10 yr)"}}, + {"command":"set","path":"/DCF/C4","props":{"formula":"SUMPRODUCT(FCF!B11:K11/(1+WACC)^FCF!B2:K2)","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B5","props":{"value":"PV of terminal value"}}, + {"command":"set","path":"/DCF/C5","props":{"formula":"C3/(1+WACC)^10","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B6","props":{"value":"Enterprise value"}}, + {"command":"set","path":"/DCF/C6","props":{"formula":"C4+C5","bold":"true","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B7","props":{"value":"Less: Net debt"}}, + {"command":"set","path":"/DCF/C7","props":{"formula":"-NetDebt","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B8","props":{"value":"Equity value"}}, + {"command":"set","path":"/DCF/C8","props":{"formula":"C6+C7","bold":"true","font.color":"000000","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/DCF/B9","props":{"value":"Implied per-share"}}, + {"command":"set","path":"/DCF/C9","props":{"formula":"C8/SharesOut","bold":"true","numberformat":"$0.00"}} +] +EOF +``` + +**Why `SUMPRODUCT` not `NPV`.** `NPV(rate, cross_sheet_range)` silently caches `0` on v1.0.63 — ships a wrong valuation to any non-recalculating reader. `SUMPRODUCT(values/(1+rate)^periods)` is algebraically equivalent and caches correctly (period row `FCF!B2:K2 = 1..10` is a one-time setup). For irregular dates (`XNPV`), use `SUMPRODUCT(values/(1+rate)^((dates-base_date)/365))`. See §Known Issues. + +**Step 5 — 2-axis sensitivity grid (WACC × g).** 5×5 grid. Rows = WACC values `7.5% ... 11.5%`, cols = `g` values `1.5% ... 3.5%`. Each cell = one self-contained formula re-running the DCF with the grid's WACC and g substituted. Template: + +```bash +# Cell D14 (first data cell, grid anchor at C14 = WACC label, C15 = first WACC value) +# Substitute $D$13 (this cell's g) and $C15 (this cell's WACC) into a replicated EV + equity formula. +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/Sensitivity/D15","props":{"formula":"(NPV($C15,FCF!$B$11:$K$11)+(FCF!$K$11*(1+D$14)/($C15-D$14))/(1+$C15)^10+(-NetDebt))/SharesOut","numberformat":"$0.00"}} +] +EOF +``` + +Copy the formula across D15:H19 (5×5 grid). Row 14 carries g values (blue input); column C carries WACC values (blue input). Row 13 and column B carry labels. Apply 3-color gradient CF for quick-read (green = upside, red = downside): + +```bash +officecli add "$FILE" /Sensitivity --type conditionalformatting \ + --prop type=colorScale --prop ref=D15:H19 +``` + +**No Excel Data Tables.** Excel's native `/Data/Table` 2-variable table is not reliably supported via the CLI — each grid cell MUST be an explicit formula. Copy the template, do not try `Data Table` input cells. + +**Verification.** + +```bash +officecli get "$FILE" "/DCF/C8" --json | jq .format.cachedValue # equity value, plausible $ +officecli get "$FILE" "/DCF/C9" --json | jq .format.cachedValue # per-share, in $XX.XX range +officecli get "$FILE" "/Sensitivity/F17" --json | jq .format.cachedValue # grid center cell, plausible +``` + +If `C8` or `C9` cache `0`, re-set them (non-resident) — see §Build-order & cache-drift. + +### Recipe C — LBO model + +**What this recipe produces.** Sheets: `Assumptions`, `S&U` (Sources & Uses), `Debt` (multi-tranche schedule), `P&L` (5-yr), `CF`, `Exit` / `Returns`. Outputs: `MOIC`, `IRR`, and a 4-tier returns waterfall. LBO is the stress test — expect circular refs (interest ↔ cash), deepest cross-sheet chains, and the heaviest use of named ranges. + +**Build order.** `Assumptions → S&U → P&L → Debt → CF → Exit → Returns`. P&L before Debt (debt interest depends on P&L EBIT for coverage checks); Debt before CF (CF uses interest + principal amortization). Enable `calc.iterate` before Step 5. + +**Step 1 — Sources & Uses (balance required, every fee line itemized).** + +``` +Uses = Purchase_EV (EntryEBITDA × EntryMultiple) + Transaction_fees (Purchase_EV × TxnFeePct, typ 1.5–2.5%) + + Financing_fees ((Senior + Mezz) × FinFeePct, typ 1–3%) + Refinanced_debt +Sources = Senior_TLB + Mezz + Revolver_drawn + Sponsor_equity +``` + +**Sponsor equity — pick one, never both.** (a) **Stated:** `Sponsor_equity = Assumptions!SponsorEquity`, then scale senior/mezz so Sources = Uses (fees absorbed by debt, not a silent plug). (b) **Solved:** `Sponsor_equity = Uses − Senior − Mezz − Revolver − Refinanced`, label "Sponsor Equity (solved)", no standalone Assumptions ref. Hardcoded `SponsorEquity` PLUS a `=Uses − Senior − Mezz` plug guarantees silent fee absorption — stated $140M vs plug $194.67M = $54.67M unaccounted fees, CFO rejection on sight. + +```bash +# Sources = Uses hard check. +officecli set "$FILE" /S&U/B12 --prop formula='IF(ABS(SUM(B4:B7)-SUM(B9:B11))<1,"BALANCED","S&U IMBALANCE: "&ROUND(SUM(B4:B7)-SUM(B9:B11),0))' --prop bold=true + +# Stated-vs-plug consistency (Gate 4 addendum; only run if you chose pattern (a)). +STATED=$(officecli get "$FILE" /Assumptions/B12 --json | jq -r '.format.cachedValue // "null"') +PLUGGED=$(officecli get "$FILE" /S&U/B10 --json | jq -r '.format.cachedValue // "null"') # B10 = sponsor-equity row on S&U +DELTA=$(python3 -c "print(abs(float('$STATED') - float('$PLUGGED')))" 2>/dev/null || echo 99999) +python3 -c "import sys; sys.exit(0 if float('$DELTA') <= 1 else 1)" && echo "S&U sponsor OK (stated=$STATED plug=$PLUGGED)" || { echo "REJECT Gate 4 S&U: stated $STATED ≠ plug $PLUGGED (Δ=$DELTA) — fees silently absorbed"; exit 1; } +``` + +Every non-sponsor line on `S&U` is a blue Assumptions input (target EBITDA, entry multiple, fee %s) or a derived formula. No hardcoded Uses / Sources numbers. + +**Step 2 — Debt schedule (multi-tranche).** One row per tranche per year. Columns: `BeginningBalance` / `Mandatory amortization` / `Cash sweep` / `EndingBalance` / `AverageBalance` / `InterestExpense`. Senior TLB: 1% mandatory amortization + all excess cash to sweep. Mezz: 0% amortization, interest-only cash-pay. Row-map for this example (senior TLB tranche, year 2 column C): `C4=Beginning Balance, C5=Mandatory Amort, C6=Ending Balance, C7=Cash Sweep, C8=Average Balance, C9=Interest Expense`. `CF!C20` = free cash available to sweep (year-2 ending cash pre-sweep on CF sheet). Substitute your tranche row block per layout. + +```bash +# year 2 senior TLB +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/Debt/C4","props":{"formula":"B6"}}, + {"command":"set","path":"/Debt/C5","props":{"formula":"-C4*Assumptions!$B$30","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Debt/C6","props":{"formula":"C4+C5+C7","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Debt/C7","props":{"formula":"-MIN(-CF!C20,C4+C5)"}}, + {"command":"set","path":"/Debt/C8","props":{"formula":"(C4+C6)/2","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Debt/C9","props":{"formula":"-C8*Assumptions!$B$31","numberformat":"$#,##0;($#,##0);\"-\""}} +] +EOF +# Add the sweep-rule comment as a classic comment (comment is NOT a cell prop — separate --type comment). +officecli add "$FILE" /Debt --type comment --prop ref=C7 --prop text='cash sweep capped at available cash and remaining tranche balance' +``` + +**Revolver capacity cap.** If your deal uses a revolver tranche, the revolver balance each period is bounded by the commitment ceiling: +``` +Revolver_Balance = MIN(Assumptions!RevolverCapacity, MAX(0, prior_revolver + draw − paydown)) +``` +Without the `MIN(capacity, ...)` outer, a shortfall quarter silently over-draws the facility. + +Adjust row indices to your layout. Repeat for each tranche (senior / mezz / revolver) and each year. + +**Step 3 — P&L (5-year) + interest from Debt.** P&L interest row pulls from Debt: `Interest = 'Debt'!TotalInterestRowY`. This creates the **circular reference**: Interest → NI → CF → Cash Sweep → Debt balance → Interest. + +**Write-order warning.** `calc.iterate=true` governs _recalculation_, not write-phase. Appending the closing leg of a cross-sheet ring to a file that already contains the ring deadlocks the engine at 100% CPU regardless of `iterate`. For complex rings (multi-tranche LBO, revolver + TLB + mezz), use §Write-order surgery below (de-ring → write downstream → re-ring). Enable `calc.iterate=true` BEFORE writing ring formulas: + +```bash +officecli set "$FILE" / --prop calc.iterate=true --prop calc.iterateCount=100 --prop calc.iterateDelta=0.001 +``` + +`iterate` converges via successive approximation for naturally-dampening loops (higher interest → less cash → less sweep → higher balance, bounded by EBIT). `#REF!` or divergent values = pause; fix algebra, do not raise `iterateCount` to 1000. + +**Step 4 — CF + cash sweep.** Ending cash = Opening + CFO − CapEx − Mandatory amort − Cash sweep. Cash sweep = `MIN(freeCashAfterCapEx, seniorDebtBalance + seniorMandatoryAmort)`. The `MIN` cap prevents swept-below-zero. + +**Step 5 — Exit + Returns.** Row-map: `Exit: B3=Exit EV, B4=Less: remaining debt, B5=Exit equity to sponsor`; `Returns: B3=MOIC, B4=IRR`. + +```bash +# Values/formulas — single non-resident batch. +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"set","path":"/Exit/B3","props":{"formula":"'P&L'!F8*Assumptions!$B$25","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Exit/B4","props":{"formula":"-('Debt'!F6+'Debt'!F13)","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Exit/B5","props":{"formula":"B3+B4","bold":"true","numberformat":"$#,##0;($#,##0);\"-\""}}, + {"command":"set","path":"/Returns/B3","props":{"formula":"'Exit'!B5/('S&U'!B9)","numberformat":"0.00\"x\""}}, + {"command":"set","path":"/Returns/B4","props":{"formula":"IRR({-'S&U'!B9,0,0,0,0,'Exit'!B5})","numberformat":"0.0%"}} +] +EOF +# Classic comments — one --type comment per anchor cell. +officecli add "$FILE" /Exit --type comment --prop ref=B3 --prop text='Exit EV = Y5 EBITDA × exit multiple' +officecli add "$FILE" /Returns --type comment --prop ref=B3 --prop text='MOIC = exit equity / sponsor equity' +officecli add "$FILE" /Returns --type comment --prop ref=B4 --prop text='IRR — 5-yr, entry + exit only; use XIRR for mid-year dividends' +``` + +**Callout — labels: `comment` element vs Notes column vs `formula` (three distinct mechanics).** +- **Hover tooltip** → `officecli add ... --type comment --prop ref= --prop text='...'`. The **`comment` key is NOT a valid prop on `set cell`** (not in `officecli help xlsx cell` on v1.0.63) — it silently drops when embedded inside a `set cell` props dict. Use the dedicated element. +- **Visible text in an adjacent Notes column** → `{"command":"set","path":"/DCF/D3","props":{"value":"TV = FCF × (1+g) / (WACC−g)"}}` — **`value`, not `formula`**, plain quoted string. +- **Formula-style prose written as a real formula** → NEVER. `{"formula":"FCF10*(1+g)/(WACC-g)"}` produces `#NAME?` in Excel (`FCF10`, `g`, `WACC` are unbound identifiers in that cell context). + +For mid-year dividends or partial exits, use `XIRR({cashflows}, {dates})` instead of `IRR`. + +**Step 6 — Returns waterfall (optional, 4-tier LP/GP).** Tiers: (1) LP preferred return 8% ; (2) GP catch-up to 20% ; (3) 80/20 split above hurdle ; (4) 100% to LP on loss. Each tier is a `MAX(0, MIN(...))` clamp. See §Sensitivity & scenarios for the general grid pattern. + +**Verification.** + +```bash +officecli get "$FILE" /S&U/B12 --json | jq '.data.value // .data.cachedValue' # must say BALANCED +officecli get "$FILE" /Returns/B3 --json | jq .format.cachedValue # MOIC, expect 2.0x-4.0x typical +officecli get "$FILE" /Returns/B4 --json | jq .format.cachedValue # IRR, expect 0.15-0.30 typical +# Iterate converged? +officecli query "$FILE" 'cell:contains("#REF!")' --json | jq '.data.results | length' # must be 0 +``` + +## Sensitivity & scenarios + +**Three patterns, pick one:** +- **(a) Base / Upside / Downside columns** on Assumptions — side-by-side scenarios, dropdown-less switch via an "Active" column + `INDEX/MATCH`. +- **(b) Dropdown + `INDEX/MATCH` switch** — one validation dropdown on Summary drives every driver via `INDEX(Base:Downside, MATCH(Dropdown, ScenLabels, 0))`. +- **(c) 2-axis sensitivity grid** — 5×5 or 7×7, one self-contained formula per cell, row/col headers are the two drivers. See Recipe B Step 5 for WACC × g. + +Mixing (a)+(b) creates circular input (scenario picked by dropdown AND overwritten by Active column) — pick one. + +**Grid rule:** each cell substitutes row-driver and col-driver into a self-contained copy of the output formula. Cannot reference the `WACC` named range (that's the panel) — reference the grid's axis cell. + +**Dropdown scenario switch.** One `validation` dropdown on Summary drives every `Assumptions` row: + +```bash +cat <<'EOF' | officecli batch "$FILE" +[ + {"command":"add","parent":"/Summary","type":"validation","props":{"sqref":"B1","type":"list","formula1":"Base,Upside,Downside"}}, + {"command":"set","path":"/Assumptions/B5","props":{"formula":"INDEX(C5:E5,MATCH(Summary!$B$1,$C$4:$E$4,0))"}} +] +EOF +# If you want a hover tooltip on B5, add it separately: +officecli add "$FILE" /Assumptions --type comment --prop ref=B5 --prop text='Revenue growth — picked by Summary!B1 scenario dropdown' +``` + +Every `Assumptions` driver row gets the same `INDEX/MATCH`. Base / Upside / Downside columns on C:E stay blue (hardcoded scenario inputs). + +**Football-field chart pattern (DCF valuation summary).** Horizontal Low→High bars for 3–5 valuation methods (DCF base, DCF bear, Trading comps, Precedent txns, LBO floor) stacked vertically. On a `Football` sheet: col A = method label, col B = Low $, col C = High $, col D = `=C−B` (width). Chart as a stacked bar with column B as an invisible first series (white/no-fill) and column D as the visible series — `dataRange=Football!A3:D7`, `chartType=bar`. Excel reads this as a floating bar per method. + +## Financial function patterns + +Terse reference — not a finance textbook. If you don't know what these do, pause and ask the user. + +| Function | Prefer over | Why | +|---|---|---| +| `XNPV(rate, values, dates)` | `NPV` | Irregular cash flow dates (M&A close mid-year, staggered tranches) | +| `XIRR(values, dates)` | `IRR` | Irregular dates; multiple sign changes handled better | +| `INDEX(range, MATCH(lookup, key, 0))` | `VLOOKUP` | Insert-safe (VLOOKUP breaks when a column is inserted in the source range) | +| `IFERROR(x/y, 0)` or `IF(y=0, 0, x/y)` | bare division | Guard every `/` in a financial model — `#DIV/0!` shipped = delivery failure | +| `MIRR(values, financeRate, reinvestRate)` | `IRR` with sign flips | When cash-flow pattern has 2+ sign changes | +| `SUMIFS(sumRange, criteriaRange1, criterion1, ...)` | `SUMPRODUCT((...))` array | Avoids the cached-value trap on array formulas (→ xlsx v2 §Common Workflow Step 5 array-formula fallback) | + +**`SUMPRODUCT(1/COUNTIF(...))` distinct-count trap.** The CLI engine caches the inner division per-row → `1/N` (e.g. `0.001543`) rather than the true distinct count. `SUMPRODUCT(--((range<>"")/COUNTIF(range,range&"")))` pattern is likewise affected. **Fallback (from xlsx v2):** hardcode the correct distinct count with a blue font + adjacent comment `"hardcoded distinct count; update if rows change"`, and disclose at delivery. LBO deal-count or portfolio headcount from a transactions list is the typical pattern that hits this. + +**Cross-sheet `NPV()` / `XNPV()` cache-0 fallback (preferred).** When the engine caches `0` on a cross-sheet `NPV()` / `XNPV()`, replace the formula with its algebraic equivalent `SUMPRODUCT(values/(1+rate)^periods)` — same result, caches correctly, audits cleanly. This is the first-line fix, used in Recipe B Step 4 by default. For `XNPV`, the period exponent is `(dates - base_date) / 365`. + +**Cache fallback on `IRR` / `MOIC` / summary KPI cells (last resort).** If a valuation cell still ships with `cachedValue = 0` after algebraic rewrite + re-set after close, hardcode the computed value with a blue font and add a classic comment via `officecli add "$FILE" /Sheet --type comment --prop ref= --prop text='cached valuation; refreshes on open in Excel — do not edit'`. Disclose in delivery notes. Prefer re-set after close first. + +## Circular references & iterative calc + +**Enable `calc.iterate` ONLY when circularity is algebraically justified:** Interest ↔ Cash (LBO revolver / cash sweep), Tax shield ↔ NI (rare — most 3-statement models compute interest before tax and avoid), Revolver plug ↔ Ending cash (corporate cash waterfall with min-cash). + +```bash +officecli set "$FILE" / --prop calc.iterate=true --prop calc.iterateCount=100 --prop calc.iterateDelta=0.001 +``` + +`iterateCount=100` / `iterateDelta=0.001` are Excel defaults, fine for naturally dampening loops. + +### Write-order surgery (de-ring → write downstream → re-ring) + +`calc.iterate` controls recalc, not write-phase. Appending the closing leg of an already-wired cross-sheet ring (Debt.Interest ↔ CF.Cash ↔ Debt.CashSweep) deadlocks at 100% CPU; `view html` / `get` also hang on a non-converged ring. + +**3-step playbook:** +1. **De-ring** — write Debt with the 10–20 ring cells set to literal `0` (e.g. `C7=0`, not `=-MIN(...)`). Removes the ring. +2. **Write downstream** — build all non-circular chains (P&L, CF, Exit, Returns, Summary, grid) non-resident, one heredoc per sheet. Everything caches against the zeroed cells. +3. **Re-ring** — close all residents, re-set each circular cell with its real formula, one `set` per cell, non-resident. + +**Acceptance.** `get /Debt/C7 --json | jq .format.cachedValue` returns non-zero non-null. If a cell still deadlocks, leave `=0` + classic comment `"circular; recalculates in Excel on F9"`, flag at delivery. Never paper over with `iterateCount=1000`. + +**Do NOT use `iterate` as a band-aid for `#REF!` / divergent values.** Raising `iterateCount` to 1000 hides the bug and ships a plausibly-wrong value; `validate` does not catch it. Break the loop algebraically (e.g. interest on opening balance only, not average). + +**Verify convergence.** Read the loop cell, bump a driving assumption and back, re-read — values must match: + +```bash +V1=$(officecli get "$FILE" /Debt/C9 --json | jq .format.cachedValue) +officecli set "$FILE" /Assumptions/B31 --prop value=0.085 +officecli set "$FILE" /Assumptions/B31 --prop value=0.0845 +V2=$(officecli get "$FILE" /Debt/C9 --json | jq .format.cachedValue) +[ "$V1" = "$V2" ] && echo "Iterate converged" || echo "WARN: drift V1=$V1 V2=$V2 — tighten iterateDelta or check algebra" +``` + +## Audit & Delivery Gate + +**Assume there are problems.** First build is almost never correct. Run every gate below; every check must print its success line. `validate` passing is not delivery — the model can pass schema and still be wrong by a factor of 10. + +### Gates 1–3 — inherited from xlsx v2 verbatim + +→ see xlsx v2 §QA minimum cycle (Gates 1–3 cover `view issues`, error-cell query, `validate` after close). Run them first, exactly as written in xlsx v2. No financial-model-specific tweaks. + +### Gate 4 — statement integrity (3-statement & LBO) + +Balance-check and cash-reconciliation rows produced by Recipe A / C must show `OK` / `BALANCED` every period. `query` the check rows and refuse on any `IMBALANCED` / `CF !=`: + +```bash +BS_FAIL=$(officecli query "$FILE" 'cell:contains("IMBALANCED")' --json | jq '.data.results | length') +CF_FAIL=$(officecli query "$FILE" 'cell:contains("CF !=")' --json | jq '.data.results | length') +SU_FAIL=$(officecli query "$FILE" 'cell:contains("S&U IMBALANCE")' --json | jq '.data.results | length') +if [ "$BS_FAIL" -eq 0 ] && [ "$CF_FAIL" -eq 0 ] && [ "$SU_FAIL" -eq 0 ]; then + echo "Gate 4 OK (balance + recon + S&U all pass)" +else + echo "REJECT Gate 4: BS=$BS_FAIL CF=$CF_FAIL S&U=$SU_FAIL"; exit 1 +fi +``` + +If any fail, the model is silently wrong — fix the upstream chain before delivery. Most common cause: a cross-sheet formula stored `\!` (shell-mangled) — run `officecli query "$FILE" 'cell:contains("\\\\!")'` and re-enter via batch heredoc. + +### Gate 5 — cached-value sanity on valuation cells + +NPV / IRR / XIRR / equity-bridge / MOIC / summary KPI cells cached `0` = wrong number shipped to a reader who does not recalc on open. List every valuation cell and check `cachedValue`: + +```bash +# Customize the path list per recipe — this is the DCF example +for P in "/DCF/C4" "/DCF/C5" "/DCF/C6" "/DCF/C8" "/DCF/C9"; do + V=$(officecli get "$FILE" "$P" --json | jq -r '.format.cachedValue // "null"') + if [ "$V" = "0" ] || [ "$V" = "null" ]; then + echo "REJECT Gate 5: $P cached $V — re-set after close (see §Build-order & cache-drift)"; exit 1 + fi + echo "Gate 5 $P: cached=$V OK" +done +``` + +For LBO, extend the list: `/Exit/B5`, `/Returns/B3`, `/Returns/B4`. For 3-statement, extend with `/Summary/B2:B5`. + +### Gate 6 — hardcode / zone discipline + +Every Calc sheet has zero numeric hardcodes. Executable: + +```bash +HARDCODE=$(officecli query "$FILE" 'cell[type=Number]:not(:has(formula))' --json \ + | jq '[.data.results[] | select(.path | test("/(P&L|Balance Sheet|Cash Flow|DCF|Debt|FCF|WACC|Exit|Returns)/"))] | length') +[ "$HARDCODE" -eq 0 ] && echo "Gate 6 OK (no hardcodes on Calc sheets)" || { echo "REJECT Gate 6: $HARDCODE hardcoded numeric cells on Calc zone — move to Assumptions"; exit 1; } + +# Named-range coverage + dead-decoration audit: ≥3 ranges declared AND each referenced by ≥1 formula. +NR=$(officecli query "$FILE" namedrange --json | jq '.data.results | length') +[ "$NR" -ge 3 ] && echo "Gate 6 OK ($NR named ranges)" || echo "WARN Gate 6: only $NR named ranges" +DEAD=0 +for NR_NAME in $(officecli query "$FILE" namedrange --json | jq -r '.data.results[].name'); do + USES=$(officecli query "$FILE" "cell:has(formula):contains(\"$NR_NAME\")" --json | jq '.data.results | length') + [ "$USES" -ge 1 ] && echo " $NR_NAME: $USES uses OK" || { echo " WARN: $NR_NAME unused"; DEAD=$((DEAD+1)); } +done +[ "$DEAD" -eq 0 ] && echo "Gate 6 named-range audit OK" || { echo "REJECT Gate 6: $DEAD dead-decoration name(s)"; exit 1; } +``` + +### Gate 5b — visual audit via HTML preview (mandatory) + +Gates 1–4/6 are grep defenses — they cannot see a rendered sheet. Run `officecli view "$FILE" html` and Read the returned HTML. Walk every sheet (inherits xlsx v2 visual floor): + +- No `###` in any numeric cell (widen column). +- No truncated labels / section headers (widen column or `alignment.wrapText=true`). +- No placeholder tokens (`TBD`, `{var}`, `xxxx`) — Gate 6.1 grep below. +- Balance-check / recon rows say `OK` / `BALANCED` every period column. +- Dashboard charts render, y-axis = 0 on ARR/revenue lines, source data matches statement sheet. +- Sensitivity grid colors read green (upside) → red (downside) — color-scale CF applied. +- No stale cached `0` on summary KPIs; if present, run cache-refresh pass. + +REJECT on any defect. **Human preview:** `officecli watch "$FILE"`, or open in Excel / WPS / Numbers — final colors + chart fidelity only fully render in the target viewer. + +### Gate 6.1 — token / placeholder sweep + +```bash +LEAK=$(officecli view "$FILE" text | grep -niE 'TBD|\(fill in\)|xxxx|lorem|\{\{|placeholder|coming soon') +[ -z "$LEAK" ] && echo "Gate 6.1 OK (no placeholder tokens)" || { echo "REJECT Gate 6.1:"; echo "$LEAK"; exit 1; } +``` + +### Honest limit + +`validate` catches schema errors, not finance errors. A model passes `validate` with `BS.Cash` hardcoded to force balance, an `NPV` cached at `0`, a sensitivity grid all-zero because it was built before FCF, a `#NAME?` runtime on a `P&L`-named sheet with unquoted refs. Gates 4 / 5 / 6 / 5b exist because schema-level `validate` cannot catch any of this. + +## Known Issues & Pitfalls + +→ Base pitfalls (cross-sheet `!` trap, batch JSON dotted-name rule, resident + formula batch deadlock, renderer caveats, `labelRotation` / `pareto` / databar-min-max bugs, `validate` while resident): see xlsx v2 §Known Issues & Pitfalls — all apply. -## Full Guide +Financial-model-specific: -Read [creating.md](creating.md) and follow it step by step. It contains setup conventions, core financial statement patterns, advanced patterns (DCF, sensitivity, scenarios), chart recipes, QA checklist, and known issues with workarounds. +- **AP sign on COGS.** Accounts Payable: if COGS is stored negative on the P&L, AP formula must negate — `=-COGS*DaysPayable/365`. Wrong sign inflates NWC and flips CF direction. Silent; passes `validate`. +- **`#NAME?` not caught by `query` / `validate`.** A cross-sheet formula referencing `P&L!B3` without quoting the sheet name (because `&` is special) lands at runtime as `#NAME?`. Always write cross-sheet refs as `'P&L'!B3` — single-quote the sheet name if it contains `&`, space, `(`, `)`, etc. Gate 5b visual check is the only detection. +- **Iterative calc silent non-convergence.** `calc.iterate=true iterateCount=100` converges at whatever the cap lands on — even if the true answer is 2× that. Always run convergence verify (§Circular references). Complex LBO rings (multi-tranche debt + sweep + tax shield) may not converge; when `cachedValue=0` on a ring cell, use §Write-order surgery. +- **Batch-while-resident deadlock on circular writes.** Writing the closing leg of a cross-sheet ring via `batch` with a resident open deadlocks at 100% CPU. Even single `set` on a ring cell can hang. Fix: close residents, write the ring in two passes per §Write-order surgery. Non-resident single-heredoc is the only safe form. +- **Cross-sheet cached value stale in `view html`.** Downstream written in the same sequence as upstream caches `0`. Excel resolves on open; HTML preview does NOT. Re-set every downstream non-resident after the chain (§Build-order & cache-drift). +- **`NPV()` / `XNPV()` cross-sheet caches `0` on v1.0.63.** Rewrite as `SUMPRODUCT(values/(1+rate)^periods)` — algebraically equivalent, caches correctly. Applied by default in Recipe B Step 4. +- **Sensitivity-grid cache trap.** Grid built before FCF/WACC → every cell caches `0`. Build FCF + WACC + DCF first, then grid in a separate non-resident batch. Fallback: hardcode blue + comment `"hardcoded sensitivity; refresh on assumption change"`. +- **`BS.Cash` = CF ending cash always** (including Y1: `BS.Cash = 'Cash Flow'!B19`). Never an independent plug or Assumptions ref — a plugged `BS.Cash` hides balance errors. +- **Year 2+ `Opening Cash` = prior period `Ending Cash`** (`C17=B19`, `D17=C19`). Independent Y2+ opening-cash inputs silently drift from BS. +- **Waterfall chart "total" bars.** `chartType=waterfall` cannot mark total programmatically — use `colors=` convention (dark = total, medium = positive, red = negative). See `help xlsx chart`. +- **DCF per-share when `SharesOut` is a formula.** `=BasicShares + OptionPool × ExerciseAssumption` → add a blue-font assumption cell and point the `SharesOut` named range at the computed cell, not the raw input. -## References +## Help pointer -- [creating.md](creating.md) -- Complete financial model creation guide -- [xlsx SKILL.md](../xlsx/SKILL.md) -- General xlsx reading, editing, and QA reference -- [data-dashboard creating.md](../officecli-data-dashboard/creating.md) -- Batch syntax, chart presets, and CF basics +When in doubt: `officecli help xlsx [element] [--json]`. Help is the authoritative schema; this skill is the decision guide for financial-modeling deltas. diff --git a/skills/officecli-financial-model/creating.md b/skills/officecli-financial-model/creating.md deleted file mode 100644 index c218ba1ec..000000000 --- a/skills/officecli-financial-model/creating.md +++ /dev/null @@ -1,981 +0,0 @@ - - -# Creating a Financial Model - -Complete guide for building formula-driven, multi-sheet financial models from scratch. - -> **Prerequisite:** This guide assumes familiarity with the officecli xlsx building blocks from [data-dashboard creating.md](../officecli-data-dashboard/creating.md). This guide focuses on what is unique to financial models: formula chains, cross-sheet patterns, financial conventions, sensitivity tables, and scenario switching. - ---- - -## Section A: Setup and Conventions - -### A.1 What You Will Build - -A single `.xlsx` with 4-10 interconnected sheets. Dependency: `Assumptions -> IS -> BS -> CF -> Valuation/DCF -> Sensitivity -> Error Checks -> Dashboard`. - -Non-negotiable: all statement values are formulas (only Assumptions has hardcoded numbers), all cross-sheet formulas use heredoc batch, fullCalcOnLoad with iterate is always set. - -### A.2 Sheet Structure Convention - -| Sheet | Tab Color | Purpose | -|-------|-----------|---------| -| Assumptions | `4472C4` (blue) | All hardcoded inputs | -| Income Statement | `A5A5A5` (gray) | Revenue through Net Income | -| Balance Sheet | `A5A5A5` (gray) | Assets, Liabilities, Equity | -| Cash Flow | `A5A5A5` (gray) | Operating, Investing, Financing | -| Valuation / DCF | `ED7D31` (orange) | WACC, FCF, Terminal Value | -| Scenarios | `4472C4` (blue) | Dropdown + scenario assumptions | -| Error Checks | `FF0000` (red) | Balance, reconciliation, ISERROR | -| Dashboard | `70AD47` (green) | Charts and summary KPIs | - -### A.3 Financial Color Coding - -| Cell Type | Font Color | Hex | -|-----------|-----------|-----| -| Input (hardcoded) | Blue | `0000FF` | -| Formula (same sheet) | Black | `000000` | -| Cross-sheet reference | Green | `008000` | - -Apply colors in the formatting batch (step 8 of build order): - -```bash -officecli set model.xlsx /Assumptions/B3:D15 --prop font.color=0000FF -officecli set model.xlsx "/Income Statement/B3:D20" --prop font.color=000000 -``` - -### A.4 Number Format Map - -| Type | Format Code | Example | -|------|------------|---------| -| Accounting | `_($* #,##0_);_($* (#,##0);_($* "-"_);_(@_)` | `$ 1,234` / `$ (567)` | -| Currency | `$#,##0;($#,##0);"-"` | `$1,234` / `($567)` | -| Percentage | `0.0%` | `12.5%` | -| Multiples | `0.0x` | `3.2x` | -| Shares | `#,##0` | `10,000,000` | -| Per-share | `$#,##0.00` | `$14.50` | - -Use heredoc batch for all number formats to avoid shell `$` expansion. - -### A.5 Column Width Convention - -Row labels (col A): 24-28, year/data columns: 14-18, narrow helpers: 10-12. Set all sheets in one batch (step 2). - -**zsh quoting:** `col[A]` paths must use **single quotes** in shell. Double quotes cause zsh glob expansion and an error: - -```bash -# WRONG — zsh expands [A] -officecli set model.xlsx "/Assumptions/col[A]" --prop width=26 - -# CORRECT -officecli set model.xlsx '/Assumptions/col[A]' --prop width=26 -``` - -In JSON batch, no special quoting is needed (path is inside a JSON string). - -```bash -cat <<'EOF' | officecli batch model.xlsx -[ - {"command":"set","path":"/Assumptions/col[A]","props":{"width":"26"}}, - {"command":"set","path":"/Assumptions/col[B]","props":{"width":"16"}}, - {"command":"set","path":"/Assumptions/col[C]","props":{"width":"16"}}, - {"command":"set","path":"/Assumptions/col[D]","props":{"width":"16"}}, - {"command":"set","path":"/Income Statement/col[A]","props":{"width":"26"}}, - {"command":"set","path":"/Income Statement/col[B]","props":{"width":"16"}}, - {"command":"set","path":"/Income Statement/col[C]","props":{"width":"16"}}, - {"command":"set","path":"/Income Statement/col[D]","props":{"width":"16"}}, - {"command":"set","path":"/Balance Sheet/col[A]","props":{"width":"26"}}, - {"command":"set","path":"/Balance Sheet/col[B]","props":{"width":"16"}}, - {"command":"set","path":"/Cash Flow/col[A]","props":{"width":"26"}}, - {"command":"set","path":"/Cash Flow/col[B]","props":{"width":"16"}} -] -EOF -``` - -Freeze panes are INCREMENTAL (one per sheet): - -### A.6 Subtotal Row Styling - -Bold + `border.top=thin` on subtotals (Gross Profit, EBITDA). `border.top=double` on final totals (Net Income, Total Assets). Apply in formatting batch (step 8). - -### A.7 Build Order (MANDATORY) - -**Always wrap steps 2–9 in resident mode** — open once, close once, all operations run in memory: - -```bash -officecli create model.xlsx -officecli open model.xlsx # ← open immediately after create - -# Step 1: add sheets while resident (INCREMENTAL) -officecli add model.xlsx / --type sheet --prop name=Assumptions -# ... remaining sheets ... -officecli remove model.xlsx /Sheet1 - -# Steps 2–9: all set/batch/add operations here - -officecli close model.xlsx # ← close AFTER all operations -officecli validate model.xlsx # ← validate on closed file -``` - -Follow this exact 10-step sequence. Do not reorder. - -| Step | Task | Mode | -|------|------|------| -| 1 | Create workbook + all sheets — plan names upfront (cannot rename) | **INCREMENTAL** | -| 2 | Column widths + freeze panes — set on every sheet (`freeze=B2` or `freeze=B3`) | **BATCH** | -| 3 | Headers + row labels — section headers, year labels, row names | **BATCH** | -| 4 | Assumptions data — hardcoded inputs (blue font in step 8) | **BATCH** | -| 5 | Statement formulas — IS → BS → CF dependency order; verify after each sheet | **BATCH** + verify | -| 6 | Valuation / scenario formulas — DCF, sensitivity, scenario switching, error checks | **BATCH** | -| 7 | Named ranges — define for all key assumptions | **INCREMENTAL** | -| 8 | Formatting + colors — number formats, blue/black font, subtotal styling, tab colors | **BATCH** | -| 9 | Charts — cell range references, `preset=dashboard` | **INCREMENTAL** | -| 10 | Protection + calcPr + validate — lock/unlock, protect, `set / --prop calc.*`, activeTab raw-set (LAST), validate | **INCREMENTAL** | - -**Step 8 Formatting — REQUIRED sub-steps (do not skip):** - -> **Step 8e: REQUIRED — Freeze panes on all data sheets** -> -> Freeze panes MUST be applied to every sheet with data rows. Row 3 = data starts at row 3 (header + year-label rows above). Failure to freeze panes is a Hard Rule violation that will fail QA. -> -> ```bash -> # Step 8e: Freeze panes — include every sheet that has column headers -> officecli set model.xlsx "/P&L" --prop freeze=B3 -> officecli set model.xlsx "/Income Statement" --prop freeze=B3 -> officecli set model.xlsx "/Balance Sheet" --prop freeze=B3 -> officecli set model.xlsx "/Cash Flow" --prop freeze=B3 -> officecli set model.xlsx "/Assumptions" --prop freeze=B2 -> # Add any additional data sheets here (Revenue, Debt Schedule, etc.) -> ``` - -> **Step 8f: REQUIRED — Set formula cells to explicit black font** -> -> All formula cells on statement sheets MUST have `font.color=000000`. This is what visually distinguishes formula cells (black) from input cells (blue `0000FF`). Omitting this makes the color-coding convention invisible and fails the Blue/Black rule. -> -> **Step 8f applies to EVERY formula cell on EVERY statement sheet.** This includes Income Statement, Balance Sheet, Cash Flow, Assumptions (roll-forward/output rows), and any auxiliary sheets (Revenue, Debt Schedule, Valuation, etc.). Apply by row range for each sheet — do NOT apply to only one sheet or one section. -> -> ```bash -> # Step 8f: Apply black font to ALL formula cells across ALL statement sheets -> # Apply by row range for each sheet — must cover EVERY sheet with formula cells -> cat <<'EOF' | officecli batch model.xlsx -> [ -> {"command":"set","path":"/Income Statement/B3:D3","props":{"color":"000000"}}, -> {"command":"set","path":"/Income Statement/B4:D4","props":{"color":"000000"}}, -> {"command":"set","path":"/Income Statement/B5:D20","props":{"color":"000000"}}, -> {"command":"set","path":"/Balance Sheet/B3:D20","props":{"color":"000000"}}, -> {"command":"set","path":"/Cash Flow/B3:D20","props":{"color":"000000"}} -> ] -> EOF -> # Repeat for ALL formula rows on EACH sheet: -> # - Assumptions output/roll-forward rows (e.g. PP&E schedule, depreciation) -> # - Revenue / Debt Schedule / Valuation / Scenarios / Error Checks sheets -> # - Any auxiliary sheet with formula cells -> # Input cells on Assumptions sheet use 0000FF (blue) — do NOT override those with 000000. -> ``` -> -> **Pre-Delivery Verification (MANDATORY):** After applying Step 8f, verify font colors visually. -> -> **⚠️ WARNING: There is no reliable CLI command to detect font color.** `officecli view text` does NOT output font color information. A grep-based check like `grep -v "000000" | grep "formula"` is a **false negative** — it always returns 0 lines regardless of actual font colors, and will mistakenly pass a model where formula cells have no explicit color set. -> -> **Correct verification method:** -> ```bash -> # Generate HTML preview and open in browser to visually confirm font colors -> officecli view model.xlsx html > preview.html -> # Open preview.html and confirm: -> # - Formula cells appear in BLACK -> # - Input cells (Assumptions) appear in BLUE -> # Alternatively, use screenshot method (see E.8 Step 10) -> ``` -> -> Confirm visually that formula cells across all statement sheets render in black. If any formula cell appears in a non-black color (e.g., default automatic color, blue, or gray), re-apply Step 8f for that sheet and verify again. - -### A.8 Execution Strategy: When to Use Batch vs Incremental - -Financial models involve 200–400 commands. Using the right execution mode per step cuts wall-clock time by 5–10×. - -**Use INCREMENTAL (one command at a time):** -- Any `add` command — creates new structure (sheet, chart, named range, validation, color scale) -- Any `remove` command -- Any `raw-set` command -- `validate` — always run alone, read the output before continuing -- First command after each major step — confirm the structure is correct before filling it -- **When uncertain** — single commands give immediate feedback; a failed batch is harder to diagnose than a failed single command - -**Use BATCH (heredoc):** -- Any series of `set` commands — filling values, formulas, widths, formats -- This covers steps 2, 3, 4, 5, 6, 8 entirely - -**Batch chunk size: 15–20 operations max.** The known-issue "Failed to send to resident" occurs more often with larger arrays. Split by logical group (one sheet per batch, or one formula section per batch): - -```bash -# Batch template — one heredoc per logical group -cat <<'EOF' | officecli batch model.xlsx -[ - {"command":"set","path":"/Assumptions/A1","props":{"value":"Revenue Drivers","bold":true}}, - {"command":"set","path":"/Assumptions/B1","props":{"value":"Year 1","bold":true}}, - {"command":"set","path":"/Assumptions/C1","props":{"value":"Year 2","bold":true}}, - {"command":"set","path":"/Assumptions/D1","props":{"value":"Year 3","bold":true}} -] -EOF -``` - -**Cross-sheet formulas in batch:** Use the `formula` prop with `=` prefix — the `==` double-equals trick (`--prop "formula==Sheet!Cell"`) is only needed in shell to escape `!`. In JSON batch, write the formula literally: - -```json -{"command":"set","path":"/Income Statement/B3","props":{"formula":"=Assumptions!B3"}} -``` - ---- - -## Section B: Core Patterns - -### B.1 Assumptions Sheet - -Create workbook, sheets, then populate. Assumptions is the single source of truth: - -```bash -officecli create model.xlsx -officecli add model.xlsx / --type sheet --prop name=Assumptions -officecli add model.xlsx / --type sheet --prop name="Income Statement" -officecli add model.xlsx / --type sheet --prop name="Balance Sheet" -officecli add model.xlsx / --type sheet --prop name="Cash Flow" -officecli add model.xlsx / --type sheet --prop name=Dashboard -officecli remove model.xlsx /Sheet1 -``` - -Populate with sections (Revenue Drivers, Cost Drivers, Working Capital, etc.) -- year columns B/C/D. **Use batch for all cell fills** (steps 3 + 4 are pure `set` operations): - -```bash -# Step 3 + 4: Headers, row labels, and input values in one batch per section -# Professional financial models use dark header rows (deep blue/dark gray) + white font to -# visually separate section headers from data rows. Always set `fill` on header rows. -cat <<'EOF' | officecli batch model.xlsx -[ - {"command":"set","path":"/Assumptions/A1","props":{"value":"Revenue Drivers","bold":true,"font.size":11,"fill":"1F3864","font.color":"FFFFFF"}}, - {"command":"set","path":"/Assumptions/B1","props":{"value":"Year 1","bold":true,"fill":"1F3864","font.color":"FFFFFF"}}, - {"command":"set","path":"/Assumptions/C1","props":{"value":"Year 2","bold":true,"fill":"1F3864","font.color":"FFFFFF"}}, - {"command":"set","path":"/Assumptions/D1","props":{"value":"Year 3","bold":true,"fill":"1F3864","font.color":"FFFFFF"}}, - {"command":"set","path":"/Assumptions/A2","props":{"value":"Revenue Growth Rate"}}, - {"command":"set","path":"/Assumptions/B2","props":{"value":0.15}}, - {"command":"set","path":"/Assumptions/C2","props":{"value":0.12}}, - {"command":"set","path":"/Assumptions/D2","props":{"value":0.10}}, - {"command":"set","path":"/Assumptions/A3","props":{"value":"Gross Margin"}}, - {"command":"set","path":"/Assumptions/B3","props":{"value":0.70}}, - {"command":"set","path":"/Assumptions/C3","props":{"value":0.72}}, - {"command":"set","path":"/Assumptions/D3","props":{"value":0.74}} -] -EOF -# Continue with more sections (Cost Drivers, Working Capital, CapEx, Tax Rate) in separate batches -# Apply the same fill+font.color pattern to ALL section header rows across ALL sheets: -# "fill":"1F3864","font.color":"FFFFFF" — dark navy + white text (default recommended) -# "fill":"2E4057","font.color":"FFFFFF" — dark slate blue alternative -# "fill":"243F60","font.color":"FFFFFF" — dark teal-blue alternative -# Header rows WITHOUT fill look unprofessional — this is a Quality Bar requirement. -``` - -> **Header 行 fill 必须覆盖整行(A:D 或 A:G)** -> -> Section header 行(如 REVENUE、ASSETS、LIABILITIES)的深色 fill 必须应用到该行的**所有列**,而不仅限于 A 列 label 单元格。 -> 仅设置 A 列时,B/C/D 列在同行会显示白色背景 + 黑色文字,与深色 A 列相邻,对比强烈且可读性极低。 -> -> **正确做法**(batch 中同时设置整行): -> ```json -> {"command":"set","path":"/P&L/A5:D5","props":{"shd":"1F3864","color":"FFFFFF","bold":true}} -> ``` -> **错误做法**(只设置 A 列): -> ```json -> {"command":"set","path":"/P&L/A5","props":{"shd":"1F3864","color":"FFFFFF","bold":true}} -> ``` -> 规则:header 行的 fill 范围必须与数据列范围一致(年度模型 A:D,月度模型 A:AL 等)。 - -Add data validation on rates (INCREMENTAL — structural `add`): - -```bash -officecli add model.xlsx /Assumptions --type validation \ - --prop sqref=B2:D2 --prop type=decimal --prop min=0 --prop max=1 -``` - -### B.2 Income Statement - -All formulas reference Assumptions. Pattern: Revenue row refs Assumptions directly, COGS uses margin, then Gross Profit through Net Income as formulas. **Use batch** — in JSON, write formulas literally with `=` prefix (no `==` double-equals needed). - -**Column convention:** Column B = Year 1 data, Column C = Year 2 data, Column D = Year 3 data. Growth rates follow the same column alignment: `Assumptions!C2` is the Year 1→Year 2 growth rate (stored in the Year 2 column, applied to compute Year 2 revenue from Year 1). - -```bash -cat <<'EOF' | officecli batch model.xlsx -[ - {"command":"set","path":"/Income Statement/A3","props":{"value":"Revenue"}}, - {"command":"set","path":"/Income Statement/B3","props":{"formula":"=Assumptions!B3"}}, - {"command":"set","path":"/Income Statement/C3","props":{"formula":"=B3*(1+Assumptions!C2)"}}, - {"command":"set","path":"/Income Statement/D3","props":{"formula":"=C3*(1+Assumptions!D2)"}}, - {"command":"set","path":"/Income Statement/A4","props":{"value":"COGS"}}, - {"command":"set","path":"/Income Statement/B4","props":{"formula":"=-B3*(1-Assumptions!B5)"}}, - {"command":"set","path":"/Income Statement/C4","props":{"formula":"=-C3*(1-Assumptions!C5)"}}, - {"command":"set","path":"/Income Statement/D4","props":{"formula":"=-D3*(1-Assumptions!D5)"}} -] -EOF -# Verify cross-sheet refs (INCREMENTAL) -officecli get model.xlsx "/Income Statement/B3" -# Expected: formula="=Assumptions!B3" BROKEN if: formula="=Assumptions\!B3" -``` - -**SaaS Revenue Pattern:** For subscription models, use bottoms-up: Starting Customers, + New Customers, - Churned (`=Prior*ChurnRate`), = Ending Customers, Average Customers (`=(Starting+Ending)/2`), Revenue (`=AvgCustomers*ARPU*12`). Place customer metrics on Assumptions, revenue formula on IS. - -**SaaS ARR Waterfall:** For SaaS/subscription models, always include an ARR waterfall section on the Revenue or Assumptions sheet. Standard row structure: - -| Row | Label | Formula | -|-----|-------|---------| -| 1 | Beginning ARR | Hardcoded (Year 1) or `=Prior Year Ending ARR` | -| 2 | New ARR (+) | From Assumptions (new bookings) | -| 3 | Expansion ARR (+) | From Assumptions (upsell/cross-sell) | -| 4 | Churn ARR (-) | `=-Beginning ARR * ChurnRate` (negative value) | -| 5 | Net New ARR | `=New ARR + Expansion ARR + Churn ARR` | -| 6 | Ending ARR | `=Beginning ARR + Net New ARR` | - -```bash -# Example: ARR Waterfall on Revenue sheet, rows 4-9 -cat <<'EOF' | officecli batch model.xlsx -[ - {"command":"set","path":"/Revenue/A4","props":{"value":"Beginning ARR","bold":false}}, - {"command":"set","path":"/Revenue/B4","props":{"formula":"=Assumptions!B50"}}, - {"command":"set","path":"/Revenue/C4","props":{"formula":"=B9"}}, - {"command":"set","path":"/Revenue/D4","props":{"formula":"=C9"}}, - {"command":"set","path":"/Revenue/A5","props":{"value":"New ARR (+)"}}, - {"command":"set","path":"/Revenue/B5","props":{"formula":"=Assumptions!B51"}}, - {"command":"set","path":"/Revenue/C5","props":{"formula":"=Assumptions!C51"}}, - {"command":"set","path":"/Revenue/D5","props":{"formula":"=Assumptions!D51"}}, - {"command":"set","path":"/Revenue/A6","props":{"value":"Expansion ARR (+)"}}, - {"command":"set","path":"/Revenue/B6","props":{"formula":"=Assumptions!B52"}}, - {"command":"set","path":"/Revenue/C6","props":{"formula":"=Assumptions!C52"}}, - {"command":"set","path":"/Revenue/D6","props":{"formula":"=Assumptions!D52"}}, - {"command":"set","path":"/Revenue/A7","props":{"value":"Churn ARR (-)"}}, - {"command":"set","path":"/Revenue/B7","props":{"formula":"=-B4*Assumptions!B53"}}, - {"command":"set","path":"/Revenue/C7","props":{"formula":"=-C4*Assumptions!C53"}}, - {"command":"set","path":"/Revenue/D7","props":{"formula":"=-D4*Assumptions!D53"}}, - {"command":"set","path":"/Revenue/A8","props":{"value":"Net New ARR","bold":true}}, - {"command":"set","path":"/Revenue/B8","props":{"formula":"=B5+B6+B7"}}, - {"command":"set","path":"/Revenue/C8","props":{"formula":"=C5+C6+C7"}}, - {"command":"set","path":"/Revenue/D8","props":{"formula":"=D5+D6+D7"}}, - {"command":"set","path":"/Revenue/A9","props":{"value":"Ending ARR","bold":true}}, - {"command":"set","path":"/Revenue/B9","props":{"formula":"=B4+B8"}}, - {"command":"set","path":"/Revenue/C9","props":{"formula":"=C4+C8"}}, - {"command":"set","path":"/Revenue/D9","props":{"formula":"=D4+D8"}} -] -EOF -``` - -Assumptions rows needed: B50=Beginning ARR (Year 1 hardcoded), B51:D51=New ARR inputs, B52:D52=Expansion ARR inputs, B53:D53=Churn Rate (as decimal, e.g. 0.05 = 5% annual churn). Adjust row numbers to match your Assumptions sheet layout. - -> **Year 2+ Beginning ARR — 必须引用前年 Ending ARR,禁止在 Assumptions 中设独立输入** -> -> Year 2 Beginning ARR = Year 1 Ending ARR(直接公式引用,不要在 Assumptions 中设立独立的 "Year 2 Opening ARR" 输入字段)。 -> 在 Assumptions 中设立独立输入会产生双源不一致风险:若两处数值不同步,waterfall 将从 Year 2 起出现跳跃缺口。 -> -> 正确做法(参见上方代码示例): -> - `C4` = `=B9`(Year 2 Beginning ARR = Year 1 Ending ARR,引用上一年的 Ending ARR 单元格) -> - `D4` = `=C9`(Year 3 Beginning ARR = Year 2 Ending ARR) -> -> **只有 Year 1 Beginning ARR(B4)才从 Assumptions 读取**(`=Assumptions!B50`),Year 2+ 均从 waterfall 自身的 Ending ARR 行引用。 - -**Historical Actuals:** For DCF models with historical columns (2023A, 2024A), hardcode actual values with blue font (`0000FF`). Projected columns use formulas with black font. Label headers with "A" suffix for actuals, "E" for estimates. - -Continue: Gross Profit (`=B3+B4`), OpEx lines, EBITDA, D&A, EBIT, Interest, EBT, Tax (`=-EBT*TaxRate`), Net Income. Verify after each batch: - -**Sign convention (critical):** Revenue is positive. COGS, OpEx, D&A, Interest are **negative** values. This means `=Gross Profit + Total OpEx` works as a simple sum only if OpEx cells contain negative numbers. `EBITDA = =B5+B10` where B5=GrossProfit and B10=TotalOpEx (negative) gives the correct result. If OpEx rows hold positive values, use subtraction: `=B5-B10`. - -```bash -officecli get model.xlsx "/Income Statement/B3" -# Must contain "Assumptions!" without backslash -``` - -### B.3 Balance Sheet - -> **WARNING: Cash Line Circularity.** NEVER use cash-as-plug. BS Cash ALWAYS equals CF Ending Cash (`=Cash Flow!B19`), including Year 1. The CF statement handles opening balance internally (Opening Cash + Net Cash Flow = Ending Cash). Never reference Assumptions for BS Cash directly. - -```bash -officecli set model.xlsx "/Balance Sheet/A4" --prop value=Cash -officecli set model.xlsx "/Balance Sheet/B4" --prop "formula==Cash Flow!B19" -officecli set model.xlsx "/Balance Sheet/C4" --prop "formula==Cash Flow!C19" -officecli set model.xlsx "/Balance Sheet/A5" --prop value="Accounts Receivable" -officecli set model.xlsx "/Balance Sheet/B5" --prop "formula==Income Statement!B3*Assumptions!B20/365" -officecli set model.xlsx "/Balance Sheet/A6" --prop value="PP&E (net)" -officecli set model.xlsx "/Balance Sheet/B6" --prop "formula==Assumptions!B15" -officecli set model.xlsx "/Balance Sheet/C6" --prop "formula==B6+Assumptions!C16-Income Statement!C12" -officecli set model.xlsx "/Balance Sheet/A18" --prop value="Balance Check" --prop bold=true -officecli set model.xlsx "/Balance Sheet/B18" --prop formula="=ROUND(B10-B15-B17,0)=0" -``` - -> **⚠️ Balance Check — 必须使用独立计算路径(HARD RULE)** -> -> Balance check 公式必须分别独立计算 Assets 合计与 Liabilities+Equity 合计,然后做差值。**严禁使用恒真式(tautological formula)**。 -> -> | 类型 | 公式示例 | 问题 | -> |------|---------|------| -> | ANTI-PATTERN(恒真式) | `=IF(B10=B10, "Balanced", "Error")` | 两侧引用同一公式,永远为 TRUE,无论 BS 是否真正平衡 | -> | ANTI-PATTERN(恒零差) | `=SUM(B5:B10)-SUM(B5:B10)` | 同一区域相减,永远为 0 | -> | CORRECT | `=ROUND(TotalAssets - (TotalLiabilities + TotalEquity), 0) = 0` | 分别引用 Assets 小计行与 Liabilities/Equity 小计行,差值为 0 才是真正平衡 | -> -> **正确做法:** -> - `TotalAssets` = 所有资产行的独立 SUM(如 `=SUM(B4:B9)`) -> - `TotalLiabilities` = 所有负债行的独立 SUM(如 `=SUM(B12:B14)`) -> - `TotalEquity` = 所有股本行的独立 SUM(如 `=SUM(B16:B17)`) -> - Balance Check = `=ROUND(TotalAssets - (TotalLiabilities + TotalEquity), 0) = 0` -> -> 若 check 显示 FALSE,立即停止交付,检查各行公式。 - -**PP&E Roll-Forward:** Opening PP&E + CapEx - D&A = Closing PP&E. Year 1 opening = Assumptions starting PP&E. D&A formula: `=Opening*DepreciationRate` (from Assumptions). The D&A value feeds both IS (expense) and BS (reducing PP&E). Balance check (B10=Total Assets, B15=Total Liabilities, B17=Total Equity) must evaluate to TRUE. Replicate across all year columns. - -> **Opening Shareholders' Equity(期初股本)必须与期初资产平衡** -> -> Opening Equity = Opening Assets - Opening Liabilities -> = (Opening Cash + Opening PP&E net + Other Opening Assets) - (Opening Long-Term Debt + Opening Other Liabilities) -> -> 此值必须通过公式计算(引用 Assumptions 中的期初资产和负债项目),**不得直接填入估算值**。 -> -> 原因:硬编码的期初股本几乎必然与期初资产/负债不匹配,导致 Balance Check Year 1 = FALSE,且该误差会传播到所有后续年份(因为 Year 2 期初股本 = Year 1 期末股本)。 -> -> 正确做法: -> ```bash -> # Opening Equity = Opening Assets - Opening Liabilities (从 Assumptions 引用) -> officecli set model.xlsx "/Balance Sheet/B17" --prop "formula==Assumptions!B10+Assumptions!B15-Assumptions!B40" -> # 其中 B10=Opening Cash, B15=Opening PP&E, B40=Opening Long-Term Debt -> # 调整行号以匹配你的 Assumptions 布局 -> ``` -> **验证**:设置后立即检查 Balance Check 是否为 TRUE。若 Year 1 = FALSE,首先检查期初股本公式。 - -### B.4 Cash Flow Statement - -Operating (Net Income + D&A +/- NWC changes), Investing (-CapEx), Financing. Include reconciliation check: - -```bash -officecli set model.xlsx "/Cash Flow/A4" --prop value="Net Income" -officecli set model.xlsx "/Cash Flow/B4" --prop "formula==Income Statement!B20" -officecli set model.xlsx "/Cash Flow/A5" --prop value="Add: D&A" -officecli set model.xlsx "/Cash Flow/B5" --prop "formula==Income Statement!B12" -officecli set model.xlsx "/Cash Flow/A6" --prop value="Change in Working Capital" -officecli set model.xlsx "/Cash Flow/B6" --prop "formula==-(Balance Sheet!B5-0)+(Balance Sheet!B13-0)" -officecli set model.xlsx "/Cash Flow/A8" --prop value="Cash from Operations" -officecli set model.xlsx "/Cash Flow/B8" --prop formula="=SUM(B4:B6)" -officecli set model.xlsx "/Cash Flow/A11" --prop value=CapEx -officecli set model.xlsx "/Cash Flow/B11" --prop "formula==-Assumptions!B16" -officecli set model.xlsx "/Cash Flow/A15" --prop value="Net Cash Flow" -officecli set model.xlsx "/Cash Flow/B15" --prop formula="=B8+B11" -officecli set model.xlsx "/Cash Flow/A17" --prop value="Opening Cash" -officecli set model.xlsx "/Cash Flow/B17" --prop "formula==Assumptions!B10" -officecli set model.xlsx "/Cash Flow/C17" --prop formula="=B19" -officecli set model.xlsx "/Cash Flow/A19" --prop value="Ending Cash" -officecli set model.xlsx "/Cash Flow/B19" --prop formula="=B17+B15" -officecli set model.xlsx "/Cash Flow/A21" --prop value="Reconciliation Check" -officecli set model.xlsx "/Cash Flow/B21" --prop "formula==B19=Balance Sheet!B4" -``` - -Reconciliation check must evaluate to TRUE: CF ending cash = BS cash for every period. Replicate B columns across C, D for each year. - -### B.5 Cross-Sheet Formula Patterns (CRITICAL) - -> **#1 risk area.** The `!` in cross-sheet refs (e.g., `Assumptions!B3`) can be corrupted by shell escaping. ALWAYS use heredoc batch. - -> **⚠️ Sheet names with special characters** (`&`, spaces, parentheses, etc.) **MUST be wrapped in single quotes** in Excel formulas: -> - Sheet named `P&L` → reference as `'P&L'!B3` (NOT `P&L!B3`) -> - Sheet named `Income Statement` → reference as `'Income Statement'!B3` -> - Sheet names without special characters (e.g., `Assumptions`) → no quotes needed -> -> Using `P&L!B3` without quotes causes `#NAME?` errors at runtime. These errors are NOT detectable by `officecli query` or `validate` — only visible in screenshots or when opening in Excel. - -> **⚠️ Sheet names with spaces in CLI path arguments (HARD RULE)** -> -> When using `officecli get`, `officecli set`, or any command where the path includes a sheet name with spaces, the **sheet name portion must be wrapped in single quotes inside the path string**: -> -> ```bash -> # WRONG — Err:509 because shell treats the space as a separator -> officecli get model.xlsx "/Cash Flow/A1" -> -> # CORRECT — single quotes wrap only the sheet name portion -> officecli get model.xlsx "'/Cash Flow/A1'" -> officecli get model.xlsx "'/ARR Waterfall/B5'" -> -> # Also correct in shell using single-quoted outer string -> officecli set model.xlsx "'/Cash Flow/B19'" --prop formula="=B17+B15" -> ``` -> -> In JSON batch operations, paths inside JSON strings do NOT need this extra quoting — the JSON string boundary already prevents shell splitting: -> ```json -> {"command":"set","path":"/Cash Flow/B19","props":{"formula":"=B17+B15"}} -> ``` - -**Correct pattern:** - -```bash -# Sheet names WITHOUT special chars — no quotes needed -officecli set model.xlsx "/Assumptions/B3" --prop "formula==Assumptions!B3" - -# Sheet names WITH special chars (& or spaces) — must quote -officecli set model.xlsx "/KPIs/B4" --prop "formula=='P&L'!B4/Assumptions!B3" -officecli set model.xlsx "/KPIs/C4" --prop "formula=='Income Statement'!C20" -``` - -**Verification (MANDATORY after every cross-sheet batch):** - -```bash -officecli get model.xlsx "/Income Statement/B3" -# Expected: formula="=Assumptions!B3" -# BROKEN: formula="=Assumptions\!B3" <-- CORRUPTED -``` - -**Recovery if `\!` found:** Delete corrupted cells (`--prop value=""`), re-run batch, verify again. - -### B.6 Error Check Sheet - -Consolidate all integrity checks. Link to BS balance check and CF reconciliation rows, add ISERROR scan: - -```bash -officecli set model.xlsx "/Error Checks/A3" --prop value="Balance Sheet Balances?" -officecli set model.xlsx "/Error Checks/B3" --prop "formula==Balance Sheet!B18" -officecli set model.xlsx "/Error Checks/A4" --prop value="Cash Reconciles?" -officecli set model.xlsx "/Error Checks/B4" --prop "formula==Cash Flow!B21" -officecli set model.xlsx "/Error Checks/A5" --prop value="Formula Errors?" -officecli set model.xlsx "/Error Checks/B5" --prop "formula==SUMPRODUCT(--(ISERROR(Income Statement!B3:D20)))" -officecli set model.xlsx "/Error Checks/A7" --prop value="Overall Status" --prop bold=true -officecli set model.xlsx "/Error Checks/B7" --prop formula="=IF(AND(B3,B4,B5=0),\"ALL CLEAR\",\"ERRORS FOUND\")" -``` - -Extend across all year columns and include all checks in the AND formula. - -### B.7 Monthly Layout Pattern (SaaS / Subscription Models) - -For models that need monthly granularity (e.g., 3-year SaaS bottoms-up), use monthly columns instead of annual B/C/D. - -**Column mapping — 36-month layout:** - -| Year | Months | Columns | Annual Rollup | -|------|--------|---------|---------------| -| Year 1 | Jan–Dec | B–M (12 cols) | AN (or separate section) | -| Year 2 | Jan–Dec | N–Y (12 cols) | AO | -| Year 3 | Jan–Dec | Z–AK (12 cols) | AP | - -Row 3: month headers (`Jan-Y1`, `Feb-Y1`, ..., `Dec-Y3`). Row 4+: data rows. - -**Annual rollup placement:** Place annual summary columns immediately after the last month column (`AL`, `AM`, `AN`) with `SUM` formulas. Add a bold header row to distinguish: - -```bash -# Annual summary header -officecli set model.xlsx '/Revenue/AL2' --prop value="Year 1 Total" --prop bold=true -officecli set model.xlsx '/Revenue/AM2' --prop value="Year 2 Total" --prop bold=true -officecli set model.xlsx '/Revenue/AN2' --prop value="Year 3 Total" --prop bold=true - -# Annual sum (Monthly Revenue row = row 10 in this example) -officecli set model.xlsx '/Revenue/AL10' --prop formula="=SUM(B10:M10)" -officecli set model.xlsx '/Revenue/AM10' --prop formula="=SUM(N10:Y10)" -officecli set model.xlsx '/Revenue/AN10' --prop formula="=SUM(Z10:AK10)" -``` - -**Formula propagation across months:** Month 1 formulas reference Assumptions directly. Month 2+ use prior-month references. Build in batch chunks of 12 (one year per batch): - -```bash -# Year 1, months 2-12 (C through M) — all reference column B as prior month -cat <<'EOF' | officecli batch model.xlsx -[ - {"command":"set","path":"/Revenue/C4","props":{"formula":"=B7"}}, - {"command":"set","path":"/Revenue/C5","props":{"formula":"=C4*Assumptions!B4"}}, - {"command":"set","path":"/Revenue/C6","props":{"formula":"=ROUND(C4*Assumptions!B7,0)"}}, - {"command":"set","path":"/Revenue/C7","props":{"formula":"=C4+C5-C6"}}, - {"command":"set","path":"/Revenue/C8","props":{"formula":"=(C4+C7)/2"}}, - {"command":"set","path":"/Revenue/C9","props":{"formula":"=Assumptions!B6"}}, - {"command":"set","path":"/Revenue/C10","props":{"formula":"=C8*C9"}} -] -EOF -# Repeat for columns D through M (months 3-12), then N-Y (Year 2), Z-AK (Year 3) -``` - -**Downstream sheets:** Annual IS/P&L sheets reference the rollup columns (`AL`/`AM`/`AN`), not individual months: - -```bash -officecli set model.xlsx '/P&L/B4' --prop formula="=Revenue!AL10" -officecli set model.xlsx '/P&L/C4' --prop formula="=Revenue!AM10" -officecli set model.xlsx '/P&L/D4' --prop formula="=Revenue!AN10" -``` - ---- - -## Section C: Advanced Patterns - -### C.1 DCF Valuation - -**WACC + DCF in one batch** (CAPM cost of equity, after-tax cost of debt, discount factors, terminal value, equity bridge): - -```bash -officecli set model.xlsx /WACC/A3 --prop value="Cost of Equity (CAPM)" -officecli set model.xlsx /WACC/B3 --prop "formula==Assumptions!B25+Assumptions!B27*Assumptions!B26" -officecli set model.xlsx /WACC/A4 --prop value="After-Tax Cost of Debt" -officecli set model.xlsx /WACC/B4 --prop "formula==Assumptions!B28*(1-Assumptions!B29)" -officecli set model.xlsx /WACC/A5 --prop value=WACC -officecli set model.xlsx /WACC/B5 --prop "formula==Assumptions!B30*B3+(1-Assumptions!B30)*B4" -``` - -```bash -officecli set model.xlsx "/DCF Valuation/A3" --prop value="Discount Factor" -officecli set model.xlsx "/DCF Valuation/B3" --prop "formula==1/(1+WACC!B5)^1" -officecli set model.xlsx "/DCF Valuation/C3" --prop "formula==1/(1+WACC!B5)^2" -officecli set model.xlsx "/DCF Valuation/A4" --prop value="PV of FCF" -officecli set model.xlsx "/DCF Valuation/B4" --prop "formula==Free Cash Flow!B8*B3" -officecli set model.xlsx "/DCF Valuation/A6" --prop value="Terminal Value (Gordon Growth)" -officecli set model.xlsx "/DCF Valuation/B6" --prop "formula==Free Cash Flow!E8*(1+Assumptions!B31)/(WACC!B5-Assumptions!B31)" -officecli set model.xlsx "/DCF Valuation/A7" --prop value="PV of Terminal Value" -officecli set model.xlsx "/DCF Valuation/B7" --prop formula="=B6*E3" -officecli set model.xlsx "/DCF Valuation/A9" --prop value="Enterprise Value" -officecli set model.xlsx "/DCF Valuation/B9" --prop formula="=SUM(B4:F4)+B7" -officecli set model.xlsx "/DCF Valuation/A10" --prop value="Equity Value" -officecli set model.xlsx "/DCF Valuation/B10" --prop "formula==B9-Assumptions!B32" -officecli set model.xlsx "/DCF Valuation/A11" --prop value="Per Share" -officecli set model.xlsx "/DCF Valuation/B11" --prop "formula==B10/Assumptions!B33" -``` - -Extend discount factors and PV rows across all projection years (C3, D3... and C4, D4...). - -### C.2 Sensitivity Table (2-Variable) - -> **WARNING:** Each cell must contain a **self-contained formula** substituting row/column header values. No Excel DATA TABLE. This is the most verbose build section. - -Set up headers (WACC values in column A rows 4-8, TGR values in row 3 columns B-F), then build grid cells. Each cell is a self-contained formula using `$A4` (absolute column for WACC) and `B$3` (absolute row for TGR): - -```bash -officecli set model.xlsx /Sensitivity/A3 --prop value="WACC \ TGR" --prop bold=true -officecli set model.xlsx /Sensitivity/B3 --prop value=0.015 -officecli set model.xlsx /Sensitivity/C3 --prop value=0.020 -officecli set model.xlsx /Sensitivity/D3 --prop value=0.025 -officecli set model.xlsx /Sensitivity/A4 --prop value=0.080 -officecli set model.xlsx /Sensitivity/A5 --prop value=0.100 -officecli set model.xlsx /Sensitivity/A6 --prop value=0.120 -officecli set model.xlsx /Sensitivity/B4 --prop "formula==(SUM(DCF Valuation!B4:F4)+(Free Cash Flow!F8*(1+B$3)/($A4-B$3))*DCF Valuation!F3-Assumptions!B32)/Assumptions!B33" -officecli set model.xlsx /Sensitivity/C4 --prop "formula==(SUM(DCF Valuation!B4:F4)+(Free Cash Flow!F8*(1+C$3)/($A4-C$3))*DCF Valuation!F3-Assumptions!B32)/Assumptions!B33" -``` - -Replicate formula across all grid cells, building each row as a separate batch (8-12 ops). After building, add color scale for heat-map: - -```bash -officecli add model.xlsx /Sensitivity --type colorscale \ - --prop sqref=B4:F8 --prop mincolor=FFCDD2 --prop midcolor=FFFFFF --prop maxcolor=C8E6C9 -``` - -The center cell (base case WACC + TGR) must match the DCF sheet valuation -- primary correctness check. - -**Non-DCF Sensitivity (e.g., Revenue Growth vs Gross Margin -> EBITDA):** Same grid structure but each cell computes: `=BaseRevenue*(1+$A4)*B$3-FixedCosts` where $A4 = revenue growth rate, B$3 = gross margin. Reference active scenario values (Section C.3) so the grid updates with scenario switching. - -### C.3 Scenario Switching (Multi-Year, Multi-Assumption) - -Data validation dropdown in B1 drives the entire model. Structure: for EACH assumption, provide Base/Bull/Bear rows for EACH year, then an "Active" row with IF formulas. All downstream sheets reference only the Active rows. - -**Step 1: Dropdown** - -```bash -officecli set model.xlsx "/Scenarios/B1" --prop value=Base -officecli add model.xlsx /Scenarios --type validation \ - --prop sqref=B1 --prop type=list --prop formula1="Base,Bull,Bear" -``` - -**Step 2: Scenario data + Active rows** (repeat pattern for each assumption variable): - -```bash -officecli set model.xlsx /Scenarios/A3 --prop value=Revenue --prop bold=true -officecli set model.xlsx /Scenarios/A4 --prop value=" Base" -officecli set model.xlsx /Scenarios/A5 --prop value=" Bull" -officecli set model.xlsx /Scenarios/A6 --prop value=" Bear" -officecli set model.xlsx /Scenarios/A7 --prop value=" Active" --prop bold=true -officecli set model.xlsx /Scenarios/B4 --prop value=20000000 --prop font.color=0000FF -officecli set model.xlsx /Scenarios/C4 --prop value=22000000 --prop font.color=0000FF -officecli set model.xlsx /Scenarios/D4 --prop value=24000000 --prop font.color=0000FF -officecli set model.xlsx /Scenarios/B5 --prop value=24000000 --prop font.color=0000FF -officecli set model.xlsx /Scenarios/C5 --prop value=28000000 --prop font.color=0000FF -officecli set model.xlsx /Scenarios/D5 --prop value=32000000 --prop font.color=0000FF -officecli set model.xlsx /Scenarios/B6 --prop value=16000000 --prop font.color=0000FF -officecli set model.xlsx /Scenarios/C6 --prop value=17000000 --prop font.color=0000FF -officecli set model.xlsx /Scenarios/D6 --prop value=18000000 --prop font.color=0000FF -``` - -```bash -officecli set model.xlsx /Scenarios/B7 --prop formula="=IF($B$1=\"Base\",B4,IF($B$1=\"Bull\",B5,B6))" -officecli set model.xlsx /Scenarios/C7 --prop formula="=IF($B$1=\"Base\",C4,IF($B$1=\"Bull\",C5,C6))" -officecli set model.xlsx /Scenarios/D7 --prop formula="=IF($B$1=\"Base\",D4,IF($B$1=\"Bull\",D5,D6))" -``` - -Repeat the Base/Bull/Bear/Active block for each variable (OpEx, Gross Margin, CapEx, etc.). Use accounting format on monetary scenario inputs. - -**Step 3: Downstream references** -- ALL statement sheets reference only the Active rows: - -```bash -# Income Statement references Active Revenue row -{"command":"set","path":"/Income Statement/B3","props":{"formula":"=Scenarios!B7"}} -``` - -**Step 4: Sensitivity table integration** -- The sensitivity table must also reference Active scenario values so it updates when the dropdown changes. Replace hardcoded base-case references with Active row references in sensitivity formulas. - -### C.4 Cap Table and Waterfall - -**Ownership Tracking:** Cap Table sheet tracks shares and percentage ownership per class: - -```bash -officecli set model.xlsx "/Cap Table/A3" --prop value=Common -officecli set model.xlsx "/Cap Table/B3" --prop value=8000000 -officecli set model.xlsx "/Cap Table/A4" --prop value="Seed Preferred" -officecli set model.xlsx "/Cap Table/B4" --prop value=1500000 -officecli set model.xlsx "/Cap Table/A5" --prop value="Series A Preferred" -officecli set model.xlsx "/Cap Table/B5" --prop value=2500000 -officecli set model.xlsx "/Cap Table/A7" --prop value="Total Shares" -officecli set model.xlsx "/Cap Table/B7" --prop formula="=SUM(B3:B5)" -officecli set model.xlsx "/Cap Table/C3" --prop formula="=B3/B$7" -officecli set model.xlsx "/Cap Table/C4" --prop formula="=B4/B$7" -officecli set model.xlsx "/Cap Table/C5" --prop formula="=B5/B$7" -``` - -**Liquidation Preferences:** Define investment amounts (= liquidation preference 1x): - -```bash -officecli set model.xlsx "/Cap Table/D3" --prop value=0 -officecli set model.xlsx "/Cap Table/D4" --prop value=2000000 --prop font.color=0000FF -officecli set model.xlsx "/Cap Table/D5" --prop value=5000000 --prop font.color=0000FF -``` - -**Waterfall Analysis (Non-Participating Preferred):** Priority order: Series A 1x first, then Seed 1x, then pro-rata to all. Each class decides: take liquidation preference OR convert to common and share pro-rata. Formula: `=MAX(LiqPref, ExitValue * OwnershipPct)`. - -Exit scenario columns (e.g., B=$5M, C=$10M, D=$25M, E=$50M, F=$100M): - -```bash -officecli set model.xlsx /Waterfall/B2 --prop value=5000000 -officecli set model.xlsx /Waterfall/C2 --prop value=10000000 -officecli set model.xlsx /Waterfall/D2 --prop value=25000000 -officecli set model.xlsx /Waterfall/E2 --prop value=50000000 -officecli set model.xlsx /Waterfall/F2 --prop value=100000000 -officecli set model.xlsx /Waterfall/A4 --prop value="Series A Payout" -officecli set model.xlsx /Waterfall/B4 --prop "formula==MAX(Cap Table!D$5, B$2*Cap Table!C$5)" -officecli set model.xlsx /Waterfall/A5 --prop value="Seed Payout" -officecli set model.xlsx /Waterfall/B5 --prop "formula==MAX(Cap Table!D$4, B$2*Cap Table!C$4)" -officecli set model.xlsx /Waterfall/A6 --prop value="Common Payout" -officecli set model.xlsx /Waterfall/B6 --prop formula="=MAX(0, B$2-B4-B5)" -officecli set model.xlsx /Waterfall/A8 --prop value="Total Check" -officecli set model.xlsx /Waterfall/B8 --prop formula="=B4+B5+B6" -``` - -Replicate B4:B8 formulas across columns C through F for each exit scenario. The `MAX` formula handles the conversion decision automatically: when pro-rata share exceeds liquidation preference, the class converts; otherwise it takes the preference. Total Check must equal the exit value in every column. - -> **Note:** For participating preferred (double-dip), replace MAX with: LiqPref + MAX(0, (ExitValue - TotalLiqPrefs) * OwnershipPct). - -### C.5 Debt Schedule - -Running balance with interest on **opening balance** (avoids circularity): - -```bash -officecli set model.xlsx "/Debt Schedule/A3" --prop value="Term Loan A" --prop bold=true -officecli set model.xlsx "/Debt Schedule/A4" --prop value="Opening Balance" -officecli set model.xlsx "/Debt Schedule/B4" --prop "formula==Assumptions!B40" -officecli set model.xlsx "/Debt Schedule/C4" --prop formula="=B6" -officecli set model.xlsx "/Debt Schedule/A5" --prop value="Principal Payment" -officecli set model.xlsx "/Debt Schedule/B5" --prop "formula==-Assumptions!B41" -officecli set model.xlsx "/Debt Schedule/A6" --prop value="Closing Balance" -officecli set model.xlsx "/Debt Schedule/B6" --prop formula="=B4+B5" -officecli set model.xlsx "/Debt Schedule/A7" --prop value="Interest Expense" -officecli set model.xlsx "/Debt Schedule/B7" --prop "formula==B4*Assumptions!B42" -``` - -Closing balance period N = Opening balance period N+1 (continuity check). Interest on opening balance, NOT average -- avoids circularity. - -**Revolver:** Available = Facility Limit - Term Loan Outstanding. Draw/Repay = `MIN(CashShortfall, Available)`. Interest = Opening Revolver Balance * Revolver Rate. Place after Term Loan on the same Debt Schedule sheet. - -### C.6 Working Capital Model - -AR = Revenue x DSO/365, Inventory = COGS x DIO/365, AP = COGS x DPO/365. Net WC = AR + Inv - AP. Delta NWC = current period NWC - prior period NWC. Delta NWC feeds into Cash Flow from Operations. - -> **⚠️ WARNING: AP 公式符号必须取负** -> -> AP(Accounts Payable,应付账款)在资产负债表上是**负债(正值)**,但在 NWC 公式中起**减项**作用。公式必须对 COGS 取负: -> -> | 行 | 正确公式 | 错误公式 | 原因 | -> |---|---------|---------|------| -> | AR | `=Revenue * DSO/365` | — | AR 是资产,正值 | -> | Inventory | `=-COGS * DIO/365` | `=COGS * DIO/365` | COGS 为负数,取负后 Inventory 为正值 | -> | AP | `=-COGS * DPO/365` | `=COGS * DPO/365` | COGS 为负数,取负后 AP 为正值;若 COGS 含正号则直接使用 `=COGS * DPO/365` | -> -> **规则:AP 的计算结果必须为正数(表示公司欠供应商的钱)。** -> 若 COGS 行存储负值(如 `-500,000`),则 AP 公式必须取负:`=-COGS*DPO/365`。 -> 若 COGS 行存储正值(如 `500,000`),则 AP 公式直接乘:`=COGS*DPO/365`。 -> 使用错误符号会导致 NWC 高估、Cash Flow from Operations 方向错误,文件数值不可用。 - -```bash -officecli set model.xlsx "/Working Capital/A3" --prop value="Accounts Receivable" -officecli set model.xlsx "/Working Capital/B3" --prop "formula==Income Statement!B3*Assumptions!B20/365" -officecli set model.xlsx "/Working Capital/A4" --prop value=Inventory -officecli set model.xlsx "/Working Capital/B4" --prop "formula==-Income Statement!B4*Assumptions!B21/365" -officecli set model.xlsx "/Working Capital/A5" --prop value="Accounts Payable" -officecli set model.xlsx "/Working Capital/B5" --prop "formula==-Income Statement!B4*Assumptions!B22/365" -officecli set model.xlsx "/Working Capital/A7" --prop value="Net Working Capital" -officecli set model.xlsx "/Working Capital/B7" --prop formula="=B3+B4-B5" -officecli set model.xlsx "/Working Capital/A8" --prop value="Change in NWC" -officecli set model.xlsx "/Working Capital/B8" --prop formula="=-B7" -officecli set model.xlsx "/Working Capital/C8" --prop formula="=-(C7-B7)" -``` - -> **Year 1 Change in NWC:** `=-B7` (no prior period, so all Y1 NWC is a cash outflow). Year 2+: `=-(CurrentNWC - PriorNWC)`. Negative sign because NWC increase = cash outflow. - -### C.7 Break-Even Analysis - -Fixed Costs / Contribution Margin = Break-Even Units. Key formulas: `Fixed Costs = OpEx + D&A`, `Contribution Margin = Price x Gross Margin`, `Break-Even Units = IFERROR(FixedCosts/ContribMargin,0)`, `Break-Even Revenue = Units x Price`. Show for each scenario if applicable. - ---- - -## Section D: Charts - -### D.1 Financial Chart Types - -| Data Pattern | Chart Type | Use Case | -|-------------|-----------|----------| -| Revenue + margin trend | `combo` | Revenue bars (left) + Margin line (right) | -| Values over time | `column` | Revenue by year or scenario comparison | -| Trend line | `line` | Cash balance, cumulative FCF | -| Cash progression | `area` | Cash balance over time | -| P&L bridge | `waterfall` | Revenue breakdown, cost waterfall | - -### D.2 Chart Recipes - -Always use cell range references and `preset=dashboard`. Layout: left chart x=0, right chart x=11. - -```bash -officecli add model.xlsx /Dashboard --type chart \ - --prop chartType=column --prop title="Revenue by Year" \ - --prop series1.name="Revenue" --prop series1.values="Income Statement!B3:D3" \ - --prop series1.categories="Income Statement!B2:D2" \ - --prop preset=dashboard --prop axisNumFmt='$#,##0' \ - --prop x=0 --prop y=1 --prop width=10 --prop height=15 -``` - -### D.3 Dual-Axis Combo Chart - -`comboSplit=1` = first N series are bars, rest are lines. `secondary=2` = series 2 on right axis. Always use both. - -```bash -officecli add model.xlsx /Dashboard --type chart \ - --prop chartType=combo --prop title="Revenue & EBITDA Margin" \ - --prop series1.name="Revenue" --prop series1.values="Income Statement!B3:D3" \ - --prop series2.name="EBITDA Margin" --prop series2.values="Income Statement!B16:D16" \ - --prop series1.categories="Income Statement!B2:D2" \ - --prop comboSplit=1 --prop secondary=2 --prop preset=dashboard \ - --prop x=0 --prop y=17 --prop width=10 --prop height=15 -``` - ---- - -## Section E: Quality and Polish - -### E.1 Named Ranges - -Required for key assumptions. Define after formulas are in place. The presence of named ranges determines the calcPr xpath (always `//x:definedNames` for financial models). - -```bash -officecli add model.xlsx / --type namedrange --prop name=RevenueGrowth --prop ref="Assumptions!B2:D2" -officecli add model.xlsx / --type namedrange --prop name=GrossMargin --prop ref="Assumptions!B5:D5" -officecli add model.xlsx / --type namedrange --prop name=TaxRate --prop ref="Assumptions!B8:D8" -officecli add model.xlsx / --type namedrange --prop name=WACC --prop ref="WACC!B5" -officecli add model.xlsx / --type namedrange --prop name=TerminalGrowth --prop ref="Assumptions!B31" -``` - -### E.2 Freeze Panes, Validation, Protection, Grouping, Print Area - -Apply these to every sheet as part of build step 8-10: - -```bash -# Freeze panes (every sheet): freeze=B2 or freeze=B3 -officecli set model.xlsx "/Assumptions" --prop freeze=B2 -officecli set model.xlsx "/Income Statement" --prop freeze=B3 -# Data validation: type=list (dropdowns), type=decimal (rates 0-1), type=whole (counts) -officecli add model.xlsx /Assumptions --type validation \ - --prop sqref=B2:D2 --prop type=decimal --prop min=0 --prop max=1 -# Protection: unlock inputs, then protect each sheet -officecli set model.xlsx "/Assumptions/B2:D15" --prop locked=false -officecli set model.xlsx "/Income Statement" --prop protect=true -# Row grouping: outline=1 on detail rows under subtotals -officecli set model.xlsx "/Income Statement/row[5]" --prop outline=1 -# Print area -officecli set model.xlsx "/Income Statement" --prop printArea="A1:D25" -``` - -### E.7 raw-set Final Steps (MUST BE LAST) - -> **CRITICAL:** Run after ALL sheets, formulas, charts, named ranges, and protection. - -```bash -# Active tab (0-based) -officecli raw-set model.xlsx /workbook \ - --xpath "//x:sheets" --action insertbefore \ - --xml '' -# calcPr -- use high-level set API (do NOT use raw-set, which creates duplicate calcPr elements) -officecli set model.xlsx / --prop calc.fullCalcOnLoad=true --prop calc.iterate=true --prop calc.iterateCount=100 --prop calc.iterateDelta=0.001 -# Validate immediately -officecli validate model.xlsx -``` - -If validation fails, verify named ranges with explicit path: `officecli get model.xlsx '/namedrange[1]'`. Note: `--depth 1` tree output does NOT include named ranges — always use `/namedrange[N]` path for verification. - -### E.8 QA Checklist - -Copy-pasteable sequence. Run after every build: - -```bash -officecli validate model.xlsx # 1. Zero errors -officecli query model.xlsx 'cell:contains("#REF!")' # 2. Formula errors -officecli query model.xlsx 'cell:contains("#DIV/0!")' -officecli query model.xlsx 'cell:contains("#VALUE!")' -officecli query model.xlsx 'cell:contains("#NAME?")' -officecli query model.xlsx 'cell:contains("#N/A")' -officecli get model.xlsx "/Income Statement/B3" # 3. Cross-sheet integrity -officecli get model.xlsx "/Cash Flow/B4" # (no backslash before !) -officecli get model.xlsx "/Balance Sheet/B18" --json # 4. Balance check = TRUE -officecli get model.xlsx "/Cash Flow/B21" --json # 5. Cash reconciliation = TRUE -officecli get model.xlsx '/namedrange[1]' # 6. Named ranges exist (--depth 1 does NOT show them) -officecli get model.xlsx '/Dashboard/chart[1]' --json # 7. Charts have data -officecli get model.xlsx "/Income Statement/B20" --json # 8. Key cells are formulas -officecli view model.xlsx text # 9. Visual check (formulas blank = normal) - -# 10. MANDATORY screenshot check — required when model contains sheet names with special chars -# officecli query and validate CANNOT detect #NAME? runtime errors caused by unquoted sheet refs. -# These errors are ONLY visible in screenshots or when opening in Excel/WPS. -# Run this step if ANY sheet name contains: & + spaces + parentheses (e.g. "P&L", "Income Statement") -cd /Users/veryliu && node /Users/veryliu/Documents/GitHub/OfficeCli/scripts/screenshot.mjs xlsx model.xlsx ./screenshots -# Then visually inspect screenshots for: #NAME? #REF! #VALUE! #DIV/0! -# Pay special attention to: KPIs sheet, Error Checks sheet, any sheet that cross-references P&L or -# sheets with special characters in their names. -# If #NAME? appears: check that all cross-sheet formulas use single-quoted sheet names (see B.5). -``` - -> **⚠️ 为什么必须截图?** `officecli query` 扫描的是存储的公式字符串(如 `=P&L!B3`),不执行公式。而 `=P&L!B3`(无引号)在 Excel 运行时会求值为 `#NAME?`,但在 CLI 层面完全透明。只有通过截图或实际打开文件才能发现此类运行时错误。**含特殊字符 sheet 名的模型,截图验证是强制步骤,不可跳过。** - ---- - -## Section F: Known Issues and Workarounds - -| # | Issue | Workaround | -|---|-------|------------| -| F-1 | `!` escaping in cross-sheet formulas | Always use heredoc batch. Verify with `officecli get`. If `\!` appears, delete and re-run. | -| F-2 | Batch failure at scale | Use resident mode (`open`/`close`) + batch chunks of 15–20 ops. If a batch still fails with "Failed to send to resident", split into smaller chunks and retry. Fall back to individual commands only as last resort. | -| F-3 | calcPr duplicate elements | Use `set / --prop calc.fullCalcOnLoad=true` (high-level API). Do NOT use raw-set to insert calcPr — it creates duplicates. | -| F-3a | Sheet names with `&` or spaces cause `#NAME?` | Wrap in single quotes: `'P&L'!B3`, `'Income Statement'!C4`. Plain `P&L!B3` fails silently — error only visible in screenshots, not in `validate` or `query`. | -| F-4 | No auto-fit column width | Set explicitly: labels=24-28, numbers=14-18. | -| F-5 | Cannot rename sheets | Plan names upfront. Create with correct name. | -| F-6 | Sensitivity tables are manual | Each cell = explicit self-contained formula. Build row-by-row in separate batches. | -| F-7 | Chart series fixed at creation | Plan all series before `add`. Delete and recreate if wrong. | -| F-8 | Formula cached values blank | `view text` shows blank for formulas. Normal. fullCalcOnLoad ensures calc on open. | -| F-9 | formulacf no font.bold | Use `fill` + `font.color` only. `font.bold` causes validation errors. | -| F-10 | Number format `$` quoting | Use heredoc batch or single quotes: `--prop numFmt='$#,##0'`. | -| F-11 | Waterfall chart totals | Cannot mark as totals. Use totalColor property for visual convention. | -| F-12 | Circular references | Set `iterate="1"` in calcPr. Avoid: use prior-period cash + net CF, interest on opening balance. | -| F-13 | Chart title `$` stripping | Shell expands `$` in `--prop title`. Use heredoc batch for chart titles containing `$`, or omit `$` from titles (e.g., "Exit Waterfall (50M)" not "Exit Waterfall ($50M)"). | -| F-14 | Tautological balance check formula | Balance check formulas must use **independent calculation paths**. ANTI-PATTERN: `=B_assets - B_assets + 1` always equals 1 regardless of whether the BS balances — this is a tautology that provides zero verification value. CORRECT: `=ROUND(TotalAssets - (TotalLiabilities + TotalEquity), 0) = 0` — this equals TRUE only when the BS truly balances. See Pitfall note below. | - -> **⚠️ Balance Check Formula Anti-Pattern** -> -> Balance check formulas must be independently verifiable. A tautological formula defeats the entire purpose of the check: -> -> | | Formula | Result | Problem | -> |---|---------|--------|---------| -> | ANTI-PATTERN | `=B19-B19+1` | Always `1` | References the same cell twice — algebraically always equals 1 regardless of actual balance | -> | ANTI-PATTERN | `=SUM(B5:B10)-SUM(B5:B10)` | Always `0` | Same issue — always balanced, never catches errors | -> | CORRECT | `=ROUND(B_total_assets - (B_total_liabilities + B_total_equity), 0) = 0` | `TRUE` only when BS balances | References independent cell ranges for assets vs. liabilities+equity | -> -> The correct formula must reference **separate cell ranges** for total assets and total liabilities+equity. If either side is misstated, the formula evaluates to FALSE — which is the intended behavior. diff --git a/skills/officecli-pitch-deck/SKILL.md b/skills/officecli-pitch-deck/SKILL.md index c8e32915d..007c10e87 100644 --- a/skills/officecli-pitch-deck/SKILL.md +++ b/skills/officecli-pitch-deck/SKILL.md @@ -1,278 +1,808 @@ --- -# officecli: v1.0.24 name: officecli-pitch-deck -description: "Use this skill when the user wants to create a pitch deck, investor presentation, product launch deck, sales presentation, or business proposal in PowerPoint format. Trigger on: 'pitch deck', 'investor deck', 'Series A deck', 'product launch presentation', 'sales deck', 'fundraising deck', 'startup pitch', 'business proposal slides', 'seed pitch', 'enterprise sales deck'. Output is always a single .pptx file. This skill does NOT use morph transitions -- for morph-animated presentations, use the morph-ppt skill instead." +description: "Use this skill when the user is building a fundraising / investor pitch deck — seed, Series A / B / C, convertible note, SAFE round, strategic raise. Trigger on: 'pitch deck', 'investor deck', 'Series A deck', 'Series B deck', 'Series C deck', 'fundraising deck', 'seed pitch', 'VC deck', 'raising capital', 'term sheet presentation'. Output is a single .pptx. This skill is a scene layer on top of officecli-pptx — inherits every pptx v2 rule (visual floor, grid, palettes, connector canon, Delivery Gate). DO NOT invoke for a generic board review, sales deck, all-hands, or product launch — route those to officecli-pptx base." --- -# Pitch Deck Skill +# OfficeCLI Pitch Deck Skill -Create professional pitch presentations from scratch -- investor decks, product launches, enterprise sales decks, and business proposals. Output is a single `.pptx` file with gradient backgrounds, modern charts, styled tables, stat callouts, and speaker notes on every content slide. +**This skill is a scene layer on top of `officecli-pptx`.** Every pptx hard rule — visual delivery floor (title ≥ 36pt / body ≥ 18pt / title ≥ 2× body), 12-column grid on 33.87×19.05cm, 4 canonical palettes, chart-choice decision table, connector canon (`shape` / `from` / `to` / `tailEnd=triangle`), shell escape, resident + batch, Delivery Gate 1–5a — is inherited, not re-taught. This file adds only what **fundraising** needs on top: stage diagnosis (A / B / C), 5 赛道 arc templates, 10 key-slide recipes (cover / problem / solution / market / product / model / traction / team / financials / ask), pitch-specific numbers convention, a VC ship-check, and a pitch-specific fresh-eyes Gate 6. ---- +When the pptx base rules cover it, the text here says `→ see pptx v2 §X`. Read `skills/officecli-pptx/SKILL.md` first if you have not. + +## Setup + +If `officecli` is missing: + +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` -## BEFORE YOU START (CRITICAL) +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. -**Every time before using officecli, run this check:** +## ⚠️ Help-First Rule + +**This skill teaches what a fundraising deck requires, not every command flag.** When a prop name, enum value, or preset is uncertain, consult help BEFORE guessing. ```bash -if ! command -v officecli &> /dev/null; then - echo "Installing officecli..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - # Windows: irm https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.ps1 | iex -else - CURRENT=$(officecli --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - LATEST=$(curl -fsSL https://api.github.com/repos/iOfficeAI/OfficeCLI/releases/latest | grep '"tag_name"' | sed -E 's/.*"v?([0-9.]+)".*/\1/') - if [ "$CURRENT" != "$LATEST" ]; then - echo "Upgrading officecli $CURRENT -> $LATEST..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - else - echo "officecli $CURRENT is up to date" - fi -fi -officecli --version +officecli help pptx # All pptx elements +officecli help pptx # Full schema (e.g. chart, shape, connector, picture) +officecli help pptx --json # Machine-readable ``` ---- +Help reflects the installed CLI version. When this skill and help disagree, **help wins.** Every `--prop X=` in this file has been grep-verified against `officecli help pptx ` — if help adds / renames a prop in a later version, trust help. -## Use When +## Mental Model & Inheritance -- User wants to create an investor pitch deck (seed, Series A/B/C) -- User wants a product launch or feature announcement presentation -- User wants an enterprise sales deck or client-facing pitch -- User wants a business proposal or strategy presentation in slides -- User mentions "pitch deck", "investor deck", "sales deck", "fundraising presentation" +**Inherits pptx v2.** You should have read `skills/officecli-pptx/SKILL.md` first. This skill assumes you know how to: add slides + shapes + charts + connectors; address by `@name=` / `@id=`; quote paths; use `batch` heredocs; write `--prop tailEnd=triangle` on every flow connector; and run the 5-gate Delivery Gate. If any of those are unfamiliar, open a pptx v2 session before continuing. -## Don't Use When +## Shell & Execution Discipline -| User Request | Correct Skill | -|-------------|--------------| -| Morph-animated or cinematic presentations | morph-ppt | -| Edit/modify an existing .pptx | officecli-pptx (editing.md) | -| Excel dashboard or data report | officecli-data-dashboard | -| Word document | officecli-docx | -| Request is primarily about animation/motion effects | morph-ppt | +**Shell quoting, incremental execution, `$FILE` convention** → see pptx v2 §Shell & Execution Discipline. Same rules verbatim — quote `[N]` paths, single-quote values containing `$` (including `$35M`, `$1.2B TAM` in a cover or ask slide), never hand-write `\$ \t \n` in executable examples, one command at a time. Examples below use `$FILE` (`FILE="deck.pptx"`). -### pitch-deck vs morph-ppt +**Single-quote every shape text containing `$`.** `--prop text="Series B · $35M"` (double quotes) is WRONG — zsh expands `$35M` → empty, deck renders `Series B · M` silently. `--prop text='Series B · $35M'` (single quotes) is right. This is the #1 pitch-deck shell-escape failure mode (`$35M`, `$18M ARR`, `$1.2B TAM` appear on cover/ask/financials/milestones). Gate 2 cannot detect a stripped `$35M` — no residue. Gate 2b catches common strip patterns; single-quoting PREVENTS them. -| Aspect | pitch-deck (this skill) | morph-ppt | -|--------|------------------------|-----------| -| Core mechanic | Layout diversity + content density | Morph transition + scene actors | -| Slide construction | Build each slide fresh from scratch | Clone + ghost + modify actors | -| Animation | Standard transitions (fade, push) | Morph (shape-matching across slides) | -| Naming convention | No special naming | `!!actor` + `#sN-content` | -| Data visualization | Charts, tables, stat callouts | None (text + shapes only) | -| Helper scripts | None needed | morph-helpers.sh required | +## What "pitch deck" means here (identity) ---- +A pitch deck is a pptx with a **fundraising layer** on top: VC-oriented narrative arc, verifiable metrics, stage-appropriate data density, founder-credibility surface. Slides are consumed at ~3 seconds per slide in a live room — the pptx v2 rule. Pitch decks add a second constraint on top: **every slide carries one investable proposition**. If a slide is "interesting background" that doesn't move the ask forward, cut it. VCs will not. The base pptx rules still apply; pitch decks add six deltas: -## Hard Rules (Non-Negotiable) +1. **Stage determines everything.** Series A / B / C each dictates slide count, narrative weight, which metrics are must-haves, and tolerance for unit-econ sophistication. A Series A deck with 6 pages of CAC/LTV math reads as over-packaged; a Series B deck missing unit econ reads as incomplete. Pick the stage first — everything downstream follows. +2. **Narrative arc beats feature dump.** 10 essential slides in a fixed order: cover → problem → solution → market → product → model → traction → team → financials → ask. Out of order = VCs disengage. +3. **Numbers are a contract.** TAM/SAM/SOM must be clean three-layer; CAC/LTV must have a payback line; ARR ≠ revenue; Use-of-Funds must be a four-bucket pie. Sloppy numbers = round dies. +4. **Team slide carries prior companies.** Avatar grid alone reads as a student project. Add prior-company logos / names + one-line role. Without this, first-time founders look exactly like first-time founders. +5. **Traction chart y-axis starts at 0.** A "hockey stick" starting at `y_min = 80% of current` is a visual lie — VCs who have seen 10,000 decks spot it in < 2 seconds. +6. **The ask is a slide, not a footnote.** `$XX M` hero + four-bucket Use-of-Funds + runway length. "We're raising some money" is not an ask. -These rules are checked by evaluators and will **FAIL** the deck if violated. No exceptions beyond the explicit list below. +### Reverse handoff — when to go BACK to pptx base -### H-FONT: Body Text Minimum Size + Font Consistency +Stay in **pptx v2 base** for board reviews, all-hands, sales decks, product launches, training decks — anything not tied to raising capital. Use **this skill** only when: (a) the user mentions a specific round (seed / Series A / B / C) or a VC meeting, AND (b) the deck needs at least 4 of {problem, traction, team with credentials, Use-of-Funds, stage-appropriate unit econ, financial projections}. -**ALL body text must be ≥ 16pt.** This applies to EVERY visible text element unless it falls into the explicit exceptions below. +If the user says "fundraising deck" but the context is a corporate BU quarterly ask, that is a board review. Route to pptx v2 Recipe (d) 10-slide blueprint. If the user says "board review" but the context is a small company raising a bridge round, route here. -**Elements that MUST be ≥ 16pt (no exceptions):** -- Card body copy / card descriptions -- Flow diagram node description text -- Team bio text -- Timeline step labels -- Business model card copy -- Sub-labels under stat callout numbers (e.g., "Total Addressable Market") -- Any paragraph or supporting text block +## Series A / B / C stage diagnosis (decision tool) -**Permitted exceptions (only these — nothing else):** -- Chart axis labels / tick labels: 10pt minimum (e.g., `axisFont="10:64748B:Calibri"`) -- Chart legend text: 10pt minimum (e.g., `legendFont="10:64748B:Calibri"`) -- Table header/body rows: 11-12pt minimum (table cells have constrained space) -- Icon badge initials (letters inside avatar ellipses): any size acceptable +**Read this before writing a single command.** Pick the row that matches the user's description — everything downstream (slide count, which metrics, which recipes, what the team slide must show) derives from this one call. -**If you are unsure whether something is an exception: it is NOT. Set it to 16pt.** +| Stage | Revenue band | Team | Slide count | Dominant narrative (weight) | Must-have data | Common red flag | +|---|---|---|---|---|---|---| +| **Seed** | $0 – $1M ARR (often pre-rev) | 2 – 8 FTE | 10 – 12 | Problem (30%) + Solution (25%) + Team (15%) + Market (15%) + Traction (15%) | Founder-market fit story; 1 – 2 design-partner / pilot logos; top-down TAM ok | Over-claiming traction (10 customers = "market proven") | +| **Series A** | $1 – $5M ARR | 10 – 25 FTE | 12 – 16 | Problem (20%) + Solution (20%) + **Market "why now"** (15%) + Product (15%) + Traction (20%) + Team (10%) | PMF proof (NRR > 110%, low churn), bottom-up TAM/SAM, pipeline / pilots converted | Bottom-up TAM feels fabricated; CAC not yet meaningful but shown anyway | +| **Series B** | $5 – $30M ARR | 30 – 100 FTE | 18 – 22 | **Traction + Unit econ (30%)** + Market + Product + Team + Financials (ask) | ARR curve starting at 0; NRR, CAC, LTV, payback (< 18 mo ideal); cohort retention; logo wall | No unit-econ slide; CAC payback > 24mo without explanation; Use-of-Funds missing % | +| **Series C** | $30M+ ARR | 100+ FTE | 20 – 24 | **Financials + Scale + Moat (40%)** + Market expansion + Team depth | Multi-year GAAP, rule-of-40, GM trajectory, international expansion plan, defensibility | No moat slide; revenue growth without margin story; team slide has no prior CEO / CFO | +| **Bridge / SAFE** | any | any | 8 – 10 | **Specific bridge reason** + runway math + commitments | Prior round context; specific milestone the bridge funds; committed investor amount | Treating a bridge like a Series A — too many slides dilutes the ask | -**Font consistency (enforced across every shape):** +**Decision procedure.** From one or two user sentences ("Series B, $18M ARR, 120 customers, $35M raise"), pick exactly one stage row. All later choices in this skill reference your stage: which 赛道 template to pull, which recipes are mandatory vs optional, and which Delivery Gate 6 checks fire. -The font pairing declared in creating.md Section A.3 (e.g., Georgia for titles, Calibri for body) applies to EVERY text shape in the entire deck without exception. This includes: -- Connector labels (if any) -- Footer shapes -- Page number shapes -- Small caption or source-credit shapes -- Notes callout shapes +**Corner cases.** Bridge rounds & convertibles between A → B are closer to A or B depending on whether the bridge milestone is "finish PMF" (A shape) or "hit unit-econ target" (B shape). "Extension" rounds at the same stage reuse the earlier stage's skeleton and add a one-slide "progress since last round" update. -❌ BANNED: Using any font not declared in your A.3 pairing (e.g., adding Arial, Helvetica, Times New Roman, or any other font mid-deck) -❌ BANNED: Omitting the `font` property on any shape and relying on PowerPoint defaults +**Non-SaaS stage overrides.** The ARR / unit-econ shape of Series B fits SaaS. For other verticals, substitute revenue band + unit-econ equivalent + Gate 6.3 grep: -Before delivery, scan every `add` command in your script and confirm every shape has an explicit `"font"` value matching your declared pairing. +| Vertical | Revenue "band" at Series B | "Unit econ" equivalent | Gate 6.3 substitute | +|---|---|---|---| +| **Bio / Clinical-stage** | pre-rev, 20–60 FTE | burn rate + runway to next milestone (IND / Ph1 readout / BLA) | `shape:contains("ORR")` OR `contains("Pipeline")` OR `contains("BLA")` OR `contains("runway")` ≥ 1 | +| **Deep Tech / Frontier** | pre-rev or early pilot rev | technical milestones + TRL level + benchmark vs SoTA | `shape:contains("TRL")` OR `contains("benchmark")` ≥ 1 | +| **Marketplace / Network** | GMV $10–100M | take rate + cohort retention + liquidity | `shape:contains("GMV")` + `contains("take rate")` ≥ 1 | +| **Consumer hardware** | $2–15M revenue (shipped units) | contribution margin + repeat rate + blended CAC | `shape:contains("repeat")` OR `contains("contribution")` ≥ 1 | -### H-OVERLAP: No Shape May Overlap Another Shape's Text +Substitute the analogue grep when running Gate 6.3 on these verticals. False WARN on SaaS CAC/LTV = expected; real concern = vertical-specific analogue present. Bio Series B decks especially: burn + runway-to-milestone IS the "unit econ" story. -After adding connectors and arrows, **immediately take a PNG screenshot** and verify: -1. Every connector is visible (not missing) -2. No connector or arrow overlaps any text box content +## 赛道 arc templates (5 families) -If a connector overlaps text, reposition the connector OR send it behind the text box using `--prop zorder=back`. See creating.md C.9 for the full connector verification protocol. +5 mainstream verticals. Each one has different slide weights because what VCs require as proof-of-concept differs. Pick the vertical row; the slide skeleton is a copy-able starting point. Slide counts assume the matching stage row above. -### H-PALETTE: Color Palette Discipline +### (1) B2B SaaS / Enterprise software -Use ONLY colors from your pre-defined palette (`$PRIMARY`, `$SECONDARY`, `$ACCENT1`, `$ACCENT2`, etc.). Do NOT introduce additional accent colors not defined at the start of the script. If you used purple on Slide 3 but it was not in your initial palette definition, remove it. Every color in the deck must trace back to a named palette variable. +Canonical arc — the template most of VC muscle memory is built on. Series B example (20 slides): cover · TL;DR · problem · problem evidence · solution · product loop · market TAM/SAM/SOM · **unit economics (CAC / LTV / payback / GM)** · ARR trajectory · retention cohort · logo wall · team · competitors · financials 4-year · ask. Must-have: unit-econ slide from Series A onward; logo wall from Series B onward. -**Common traps — these WILL cause a FAIL:** +### (2) Consumer (B2C app / consumer hardware / D2C) -- ❌ BANNED: Red (`FF0000`, `CC0000`, `E53935`, etc.) to indicate "bad/wrong/competitor weakness" — use your palette's darkest color instead -- ❌ BANNED: Green (`00FF00`, `4CAF50`, `34A853`, etc.) to indicate "good/correct/your advantage" — UNLESS green is explicitly declared as a palette variable (e.g., `ACCENT1="34A853"`) -- ❌ BANNED: Gray (`888888`, `999999`, `AAAAAA`, `CCCCCC`, etc.) as a neutral/background color — use a palette light color (e.g., `$LIGHT`) instead -- ❌ BANNED: Any bare hex color value that does NOT appear in your palette variable block +Narrative-driven. Early-stage decks lean on **product-experience screenshots + founding story + "why now"** market timing; lighter on unit econ (which are usually weaker than SaaS). Series A example (14 slides): cover · hook (30-second product demo or 1-line vision) · problem (lived experience) · solution (product shots) · product-experience flow · "why now" market window · pre-order / crowdfunding / early-sales evidence · retention / engagement (DAU, D30) · market (top-down ok if bottom-up unreliable) · competitive positioning · founder story + team · press / endorsements · financials · ask. Must-have: product visuals on ≥ 3 slides; "why now" slide (window justification); engagement metric not just revenue. -**How to express contrast and status without new colors:** -- "Bad/competitor" column or row → use your palette's darkest or most muted shade (e.g., `$DARK`, `$DARK2`) -- "Good/your product" column or row → use your palette's primary or accent color (e.g., `$PRIMARY`, `$ACCENT1`) -- Neutral/disabled state → use a light-opacity version of an existing palette color, not gray -- Tick mark vs cross mark in comparison tables → use the same text color as the column; do NOT switch to red/green +### (3) Deep Tech / Frontier tech (AI foundation models, quantum, climate hardware, robotics) ---- +Technology credibility is the sell. Pre-revenue deep tech replaces "traction" with **technical milestones + defensibility**. Series B example (22 slides): cover · thesis (one-line "what changes if this works") · problem (current state of art) · solution (technical approach) · **technology architecture** · benchmarks vs SoTA · pipeline / TRL levels · market (long-tail) · business model · early commercial traction (pilots, LOIs) · IP / patents · team (usually PhD / ex-FAANG-research) · partners · financials · ask. Must-have: benchmark slide; IP slide; team slide dense with PhDs / prior-lab names. -## Core Concepts +### (4) Marketplace / Network business (two-sided platform, social, commerce) -1. **Layout-First Construction** -- Select a slide pattern from creating.md Section C, then fill with content. Never manually calculate x/y from scratch. -2. **Color Palette Upfront** -- Define 5-6 hex colors as shell variables before building. All commands reference `$PRIMARY`, `$SECONDARY`, etc. -3. **Slide Pattern Library** -- 11 pre-tested spatial blueprints with positioning tables. See [creating.md](creating.md) Section C. -4. **Chart Styling is Non-Negotiable** -- Every chart uses the modern recipe: `plotFill=none`, `chartFill=none`, subtle gridlines, `series.outline`. -5. **Speaker Notes as Checklist Item** -- Add 2+ sentence notes after each content slide is complete. +Liquidity is the metric. Replace "unit econ" with **GMV + take rate + cohort retention + supply / demand balance**. Series A example (15 slides): cover · problem (friction in current supply-demand) · solution · product demo (both sides) · network effects diagram · early liquidity (first-week GMV, time-to-match) · cohort retention · geographic / category expansion plan · competitive positioning vs incumbents · take-rate model · team · financials · ask. Must-have: liquidity metric slide; cohort retention chart; network-effect diagram. ---- +### (5) Bio / Life sciences / Healthtech + +Regulatory pipeline IS the business. Replace "product roadmap" with **clinical pipeline + regulatory path + scientific evidence**. Series B example (22 slides): cover · unmet medical need · scientific rationale (mechanism of action) · preclinical / clinical data (ORR, safety, endpoints) · **pipeline chart** (candidates × stages × dates) · differentiation vs standard of care · IP / exclusivity · regulatory strategy (IND, BTD, fast-track) · market (prevalence × pricing) · commercial strategy (orphan / specialty / biosimilar) · partnerships / collaborations · team (CSO / CMO with prior FDA wins) · financials (burn to next milestone) · ask. Must-have: pipeline chart; clinical data slide; team slide with prior regulatory wins. + +**Cross-vertical rule.** You can mix elements across templates, but never drop a must-have from your primary vertical. A SaaS deck missing unit econ, a bio deck missing a pipeline chart, a marketplace deck missing a liquidity metric — each is an instant VC disqualification. + +## Slide Patterns (layout canon) + +Patterns are **layout geometry**; recipes below are **narrative intent**. A slide picks one pattern for its visual shape (6 canonical ones below) and one recipe for what it argues (cover / problem / traction / ...). Multiple recipes can share one pattern — Problem / Why-Now / Traction-callout all lean on the 3-stat row (C.2). Pick the pattern first, then fill it with recipe content. + +**Speaker notes rule.** Every content slide (non-cover, non-closing) MUST carry speaker notes via `officecli add "$FILE" /slide[N] --type notes --prop text='…'`. Missing notes = not shippable — inherits pptx v2 §Hard rules (H7). Run `officecli help pptx notes` to confirm prop names before building. + +**Pattern reuse discipline.** Never run the same pattern on two consecutive slides — even with different data, two identical geometries in a row read as a template loop. Alternate C.2 with C.4 or C.5b to break rhythm. + +**Vertical centering.** When a slide carries fewer elements than the pattern's maximum, nudge y-positions down 2–3cm to center the visual weight. Tables below assume full content. + +### C.1 Title / Cover (dark gradient) + +3–4 text shapes on a gradient fill. Slide 1 in every deck. + +``` ++----------------------------------+ +| | +| TITLE (centered) | +| tagline | +| | +| round · amount · date | +| ________________________ | <- thin brand band ++----------------------------------+ +``` + +| Element | X | Y | Width | Height | Font / size | +|---|---|---|---|---|---| +| Title | 2cm | 5cm | 29.87cm | 4cm | serif bold, ≥ 36pt (44 typical) | +| Tagline | 2cm | 10cm | 29.87cm | 2cm | sans 18–22pt | +| Meta (round · $ · date) | 2cm | 13cm | 29.87cm | 1.5cm | sans 12–16pt | + +**Use this when** the slide is the first one (Cover recipe 1) — 3-second identity grab. Background is a 180° linear gradient between two dark palette shades (e.g. Professional Navy `1E2761 → 0D1F35`). If the title wraps to 2 lines, **add height (4cm → 5cm), never drop font below 36pt** — sub-36pt on a pitch cover reads as timid regardless of content. Transition: fade. + +### C.2 3-Stat callout row + +Title + 3 big-number / label pairs across. The default for Problem / Why-Now / Traction-callout slides. + +``` ++----------------------------------+ +| Title | +| | +| 73% 12hr $4.2B | +| label label label | +| source source source | ++----------------------------------+ +``` + +| Element | X | Y | Width | Height | Font / size | +|---|---|---|---|---|---| +| Title | 1.5cm | 1cm | 30.87cm | 3cm | serif bold ≥ 36pt | +| Stat 1 number | 2cm | 5cm | 9cm | 4cm | serif bold 60–64pt | +| Stat 1 label | 2cm | 9.5cm | 9cm | 2cm | sans ≥ 16pt (H4 floor) | +| Stat 2 number / label | 12.5cm | (same) | 9cm | (same) | (same) | +| Stat 3 number / label | 23cm | (same) | 9cm | (same) | (same) | + +**Use this when** you have 2–3 anchoring numbers and the story is "three facts argue the point" — Problem, Why-Now, Market-callout, single-row Traction. Labels ≥ 16pt is the H4 floor (sub-label exception); a number without a label reads as bravado, so never drop labels to 12–14pt to fit more text. + +### C.3 4-Stat callout row + +Same geometry as C.2 but 4 columns. Numbers 60pt, width 7cm each. + +``` ++-------------------------------------+ +| Title | +| | +| 73% 12hr $9M 4.2x | +| lbl lbl lbl lbl | ++-------------------------------------+ +``` + +| Element | X positions | Y | Width | Height | Font / size | +|---|---|---|---|---|---| +| Title | 1.5cm | 1cm | 30.87cm | 3cm | serif bold 36pt | +| Stat numbers | 1.5 / 9.5 / 17.5 / 25.5cm | 5cm | 7cm | 4cm | serif bold 60pt | +| Stat labels | (same X) | 9.5cm | 7cm | 2cm | sans ≥ 16pt | + +**Use this when** exactly 4 parallel metrics tell the story and 3 feels under-counted. Prefer C.2 if in doubt — 4 always feels tighter than 3, and wrap risk is real. + +> **Wrap warning.** At 60pt in 7cm width, dollar patterns with both `$` and `.` fail: `$9.4M` is 5 glyphs but the wide `$` and `.` in a serif bold make it wrap to 2 lines and destroy the callout. Safe dollar shapes at 60pt/7cm: `$9M`, `$96B`, `$4K` (3–4 chars). Non-dollar shapes: `340%`, `4.2x`, `12.3` safe up to 5 chars. Values ≥ 6 chars (`197min`, `3 Days`) will wrap — either (a) drop font to 44–48pt, (b) abbreviate (`197m`, `$9M`), or (c) shift to C.2 (9cm per stat). Single tokens only, no internal spaces. + +### C.4 Chart + Context (chart left, stats right) + +Chart takes left 55%, 2–3 stacked callouts on the right. The default for Traction / Financials / Market-sizing-with-context. + +``` ++-------------------------------------+ +| Title | +| | +| +---------------+ +--------+ | +| | | | Stat 1 | | +| | chart | +--------+ | +| | | | Stat 2 | | +| +---------------+ +--------+ | ++-------------------------------------+ +``` + +| Element | X | Y | Width | Height | +|---|---|---|---|---| +| Title | 2cm | 1cm | 29.87cm | 3cm | +| Chart | 2cm | 4cm | 17cm | 13cm | +| Stats column | 21cm | 4cm+ | 11cm | 2.5cm number + 1.5cm label (~3.7cm per pair) | + +Sub-labels ≥ 16pt (H4 floor). For 5 stats stacked, drop number size to 44pt; 6+ stats means pick a different pattern. Post-batch for column/bar charts: `officecli set "$FILE" "/slide[N]/chart[1]" --prop gap=80` to tighten bar spacing. + +**Use this when** one primary chart drives the story and 2–3 numeric anchors reinforce it — Traction (ARR curve + current ARR + YoY + NRR), Financials (4-year column chart + assumption callouts), Market (bar chart + SOM / CAGR / methodology). + +### C.5 Icon-in-circle grid (3-row vertical) -## Workflow Overview +3 vertical rows, each = circle icon on the left + title + 1-line description. -**Phase 1: Understand** -- Identify deck type (seed/launch/investor/sales), slide count, data provided, color preference. +``` ++---------------------------------------+ +| Title | +| | +| (o) Label one | +| description one | +| | +| (o) Label two | +| description two | +| | +| (o) Label three | +| description three | ++---------------------------------------+ +``` + +| Element | X | Y positions | Width | Height | Font / size | +|---|---|---|---|---|---| +| Icon circle | 2cm | 4.5 / 8.5 / 12.5cm | 2.5cm | 2.5cm | ellipse, accent fill | +| Label | 5.5cm | (icon Y + 0) | 25cm | 1.2cm | sans bold 18pt | +| Description | 5.5cm | (icon Y + 1.3cm) | 25cm | 1.8cm | sans ≥ 16pt (H4 floor), muted | + +**Use this when** you have 3 short vertical points that benefit from a visual anchor per row — Solution mechanism, Value pillars, Product loop. Choose C.5b (2×2 grid) when items are parallel and you have exactly 4; choose a horizontal 5-across variant when icons should read side-by-side (e.g. 5-step process). -**Phase 2: Plan** -- Select deck structure from creating.md Section A. Define palette as shell variables. Map each slide to a pattern from Section C. Verify no two consecutive slides share the same layout. +### C.5b 2×2 Feature grid (4 parallel items) -**Phase 3: Build** -- Create file, set metadata, then `officecli open` for resident mode. Build slide by slide using batch mode (heredoc syntax for 4+ elements). Apply chart/table recipes from Sections D/E. Add speaker notes after each slide. `officecli close` before QA. +4 rounded cards, 2 columns × 2 rows. Use when you have exactly 4 parallel items (product pillars, service types, feature quadrants). + +``` ++-----------------------------+ +| Title | +| | +| +---------+ +---------+ | +| | (o) T1 | | (o) T2 | | +| | body | | body | | +| +---------+ +---------+ | +| +---------+ +---------+ | +| | (o) T3 | | (o) T4 | | +| | body | | body | | +| +---------+ +---------+ | ++-----------------------------+ +``` -**Phase 4: QA & Deliver** -- Run `officecli validate`. Check the QA checklist below. Deliver with note about validation exceptions if cell merge was used. +| Element | X | Y | Width | Height | Font / size | +|---|---|---|---|---|---| +| Slide title | 2cm | 1cm | 29.87cm | 2.5cm | serif bold 32pt | +| Card 1 bg (top-left) | 1.5cm | 4cm | 14.5cm | 7cm | roundRect | +| Card 2 bg (top-right) | 17.5cm | 4cm | 14.5cm | 7cm | roundRect | +| Card 3 bg (bottom-left) | 1.5cm | 12cm | 14.5cm | 7cm | roundRect | +| Card 4 bg (bottom-right) | 17.5cm | 12cm | 14.5cm | 7cm | roundRect | +| Icon ellipse (each card) | card_x + 0.5cm | card_y + 0.5cm | 2cm | 2cm | — | +| Card title (each) | card_x + 3.2cm | card_y + 0.6cm | 10.5cm | 1.8cm | sans bold 16pt | +| Card body (each) | card_x + 0.5cm | card_y + 3cm | 13cm | 3.5cm | sans ≥ 16pt (H4 floor) | + +**Use this when** you have exactly 4 parallel items and the eye should land on each equally — 4 product pillars, 4 service tiers, 4 stakeholder types. 3 items feel lonely in a 2×2; 5+ items break the grid — go to a 3×2 (see pptx v2 §(d) grid math) or C.5 row pattern. + +> **Z-order canon (critical).** Each card's `roundRect` background must be added immediately before that card's icon / title / body shapes in the batch JSON — pptx paints in insertion order, so a background added after its text paints over and hides the text. When building with `officecli batch`, follow the per-card sequence `bg → ellipse → title → body` strictly. Pattern and z-order details → see pptx v2 §Recipe (c) z-order canon; reuse grid math from pptx v2 §(d) for non-2×2 counts. + +**Dark-background variant.** Change card fill from `F0F4F8` (light) to a lighter-dark shade like `1A2540` and bump body text to `FFFFFF` / `E8E8E8`. Palette variables (e.g. `$MUTED`) do NOT expand inside single-quoted heredocs — write the literal hex (`64748B`) in the JSON. --- -## Quick Start -- 6-Slide Seed Pitch +## Key-slide recipes (10 essentials) + +The 10 slides every pitch deck carries. Each recipe below gives: **visual outcome** (what the slide looks like from 3m away) + **runnable block** (≤ 18 lines) + **QA one-liner**. All recipes inherit pptx v2 palettes, grid math, type hierarchy, and `--prop tailEnd=triangle` on every connector. Recipes reference the Slide Patterns above: Cover reuses C.1; Problem / Why-Now reuse C.2; Traction / Financials reuse C.4; Feature / pillar slides reuse C.5b. `$FILE` is your deck file. + +**Long-title wrap rule.** A 36pt+ title that wraps to 2 lines: add `height` (e.g. 2cm → 3.5cm) — never drop the font below 36pt. Titles < 36pt on a pitch deck read as timid regardless of content. + +### (1) Cover slide — company · tagline · round · date + +**Visual outcome.** Dark navy fill, centered 44pt company name, 20pt one-line tagline underneath, small 16pt meta line at the bottom with round + amount + date. Thin brand band at the very bottom (0.5cm high) in the accent color. + +```bash +officecli add "$FILE" / --type slide --prop layout=blank --prop background=1E2761 +officecli add "$FILE" "/slide[1]" --type shape --prop name=BrandBand \ + --prop geometry=rect --prop fill=CADCFC \ + --prop x=0cm --prop y=18.5cm --prop width=33.87cm --prop height=0.55cm +officecli add "$FILE" "/slide[1]" --type shape --prop name=CoverTitle --prop text="Acme DevOps" \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=3cm \ + --prop font=Georgia --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=none +officecli add "$FILE" "/slide[1]" --type shape --prop name=Tagline --prop text="Kubernetes observability, built for production at scale" \ + --prop x=2cm --prop y=10.5cm --prop width=29.87cm --prop height=1.5cm \ + --prop font=Calibri --prop size=20 --prop color=CADCFC --prop align=center --prop fill=none +officecli add "$FILE" "/slide[1]" --type shape --prop name=CoverMeta --prop text='Series B · $35M · April 2026' \ + --prop x=2cm --prop y=15cm --prop width=29.87cm --prop height=1.2cm \ + --prop font=Calibri --prop size=16 --prop color=FFFFFF --prop align=center --prop fill=none +``` + +**QA.** Cover has 4 discrete elements (brand band + title + tagline + meta). 80%-whitespace covers fail the pptx "cover ≥ 60% filled" floor. + +**Consumer variant (3-second grab).** Consumer decks (B2C app / hardware / D2C) should add a single dominant motif — hero product shot, oversized company name (60–96pt), or symbolic mark (crescent moon / abstract geometric). Replace the 44pt title with an 80–96pt name + one motif shape (`--type shape --prop geometry=ellipse --prop fill=` for an abstract mark, or `picture` at ~40% of slide for a product hero). Keep tagline + round + date identical. SaaS / B2B may skip — the typographic-only cover is sufficient. + +### (2) Problem slide — industry pain in 1 sentence + 3 data cards + +**Visual outcome.** 36pt title stating the pain (not "The Problem"). Below, three equal-width data cards across the slide: each a giant number (40pt) + one-line qualifier (16pt) + source footnote (12pt gray). + +Grid math for 3 cards, 1.5cm margins, 0.76cm gap: `usable = 33.87 − 3 − 2·0.76 = 29.35`, `col_width = 29.35 / 3 = 9.78cm`. x-positions: `1.5 / 12.04 / 22.58`. ```bash -# Setup -officecli create pitch.pptx -officecli set pitch.pptx / --prop title="FitPulse" --prop author="Alex Kim" -officecli open pitch.pptx # Resident mode — all batch operations run in memory -PRIMARY="FF6B35"; SECONDARY="1A1A2E"; ACCENT1="00C9A7"; DARK="16213E"; MUTED="64748B" -# MUTED is declared here so stat sub-label colors use a palette variable, not a bare hex gray - -# Slide 1: Title (gradient background, pattern C.1) -cat <<'EOF' | officecli batch pitch.pptx +SLIDE=2 # second slide, after cover. Adjust from your build order. +officecli add "$FILE" / --type slide --prop layout=blank --prop background=FFFFFF +officecli add "$FILE" "/slide[$SLIDE]" --type shape --prop text="Kubernetes debugging burns 12 engineering hours / incident" \ + --prop x=1.5cm --prop y=1.2cm --prop width=30.87cm --prop height=2.5cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 --prop fill=none +cat < 1 hour to diagnose","x":"1.5cm","y":"9.5cm","width":"9.78cm","height":"3cm","font":"Calibri","size":"18","color":"333333","align":"center","fill":"none"}}, + {"command":"add","parent":"/slide[$SLIDE]","type":"shape","props":{"text":"Source: 2025 DORA Report","x":"1.5cm","y":"13cm","width":"9.78cm","height":"1cm","font":"Calibri","size":"12","italic":"true","color":"666666","align":"center","fill":"none"}} ] EOF -officecli set pitch.pptx "/slide[1]" --prop transition=fade +# Repeat the 4-block pattern at x=12.04cm and x=22.58cm for cards 2 and 3. +``` + +**QA.** `officecli query "$FILE" 'shape:contains("Source")'` returns ≥ 3 (every claim carries a source). If zero sources, VCs will not trust a single number. + +### (2b) Why Now slide — Consumer / Seed / early A must-have -# Slide 2: Market (3-stat callout, pattern C.2) -cat <<'EOF' | officecli batch pitch.pptx +**Visual outcome.** 3 cards across: each = **trigger headline** (24pt bold) + **data point** (60pt number or date) + **one-line implication** (16pt) + **source footnote** (12pt gray). Reuse Problem grid math (`col=9.78cm`, x = `1.5 / 12.04 / 22.58`). §赛道 Consumer row 2 must-have; Seed / early A in any vertical benefits when "market window" IS the thesis. + +```bash +SLIDE=3 +officecli add "$FILE" / --type slide --prop layout=blank --prop background=FFFFFF +officecli add "$FILE" "/slide[$SLIDE]" --type shape --prop text="Why now: three converging triggers" \ + --prop x=1.5cm --prop y=1.2cm --prop width=30.87cm --prop height=2.5cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 --prop fill=none +# Card 1 (x=1.5cm) — trigger / data / implication / source. Repeat at x=12.04cm and x=22.58cm. +cat <= 16pt (Hard Rule H-FONT). - {"command":"add","parent":"/slide[3]","type":"shape","props":{"text":"50K","x":"21cm","y":"4cm","width":"11cm","height":"2.5cm","font":"Georgia","size":"44","bold":"true","color":"FF6B35","align":"center","valign":"bottom","fill":"none"}}, - {"command":"add","parent":"/slide[3]","type":"shape","props":{"text":"Downloads","x":"21cm","y":"6.8cm","width":"11cm","height":"1.2cm","font":"Calibri","size":"16","color":"64748B","align":"center","fill":"none"}}, - // ^^^ "64748B" = $MUTED. Stat sub-label: must be >= 16pt (Hard Rule H-FONT). Do NOT copy 12pt from old examples. - {"command":"add","parent":"/slide[3]","type":"shape","props":{"text":"40%","x":"21cm","y":"8.5cm","width":"11cm","height":"2.5cm","font":"Georgia","size":"44","bold":"true","color":"00C9A7","align":"center","valign":"bottom","fill":"none"}}, - {"command":"add","parent":"/slide[3]","type":"shape","props":{"text":"MoM Growth","x":"21cm","y":"11.3cm","width":"11cm","height":"1.2cm","font":"Calibri","size":"16","color":"64748B","align":"center","fill":"none"}} - // ^^^ "64748B" = $MUTED. Stat sub-label: must be >= 16pt (Hard Rule H-FONT). Do NOT copy 12pt from old examples. + {"command":"add","parent":"/slide[$SLIDE]","type":"shape","props":{"text":"Preclinical","x":"8.5cm","y":"4cm","width":"5.97cm","height":"1cm","font":"Calibri","size":"16","bold":"true","color":"333333","align":"center","fill":"F5F7FA"}}, + {"command":"add","parent":"/slide[$SLIDE]","type":"shape","props":{"text":"Phase 1","x":"14.47cm","y":"4cm","width":"5.97cm","height":"1cm","font":"Calibri","size":"16","bold":"true","color":"333333","align":"center","fill":"F5F7FA"}}, + {"command":"add","parent":"/slide[$SLIDE]","type":"shape","props":{"text":"Phase 2","x":"20.44cm","y":"4cm","width":"5.97cm","height":"1cm","font":"Calibri","size":"16","bold":"true","color":"333333","align":"center","fill":"F5F7FA"}}, + {"command":"add","parent":"/slide[$SLIDE]","type":"shape","props":{"text":"Phase 3","x":"26.41cm","y":"4cm","width":"5.97cm","height":"1cm","font":"Calibri","size":"16","bold":"true","color":"333333","align":"center","fill":"F5F7FA"}}, + {"command":"add","parent":"/slide[$SLIDE]","type":"shape","props":{"text":"HLX-201 (lead)","x":"1.5cm","y":"5.5cm","width":"7cm","height":"1.5cm","font":"Calibri","size":"18","bold":"true","color":"1E2761","align":"left","fill":"none"}}, + {"command":"add","parent":"/slide[$SLIDE]","type":"shape","props":{"geometry":"roundRect","fill":"1E2761","x":"8.5cm","y":"5.7cm","width":"17.91cm","height":"1.1cm","line":"none"}} ] EOF -officecli set pitch.pptx "/slide[3]" --prop transition=push-left -officecli add pitch.pptx "/slide[3]" --type notes \ - --prop text="50K downloads in 3 months with 40% MoM growth. Premium tier growing fastest." +# Repeat rows 2 & 3 at y=7.8cm / y=10.1cm with bar widths per stage (Ph1=5.97cm, Ph1-Ph2=11.94cm, Ph1-Ph3=17.91cm). +# NCT footer full-width at y=16.8cm. +officecli add "$FILE" "/slide[$SLIDE]" --type shape --prop text='NCT05021323 (HLX-201, Ph2, n=48) · NCT06142091 (HLX-304, Ph1, n=24) · IND-filed Q1-26 for HLX-412' \ + --prop x=1.5cm --prop y=16.8cm --prop width=30.87cm --prop height=1.2cm \ + --prop font=Calibri --prop size=12 --prop italic=true --prop color=666666 --prop fill=none +``` + +**QA.** `officecli query "$FILE" 'shape:contains("NCT")' --json | jq '.data.results | length'` ≥ 1. Bar colors darken across stages (`CADCFC` preclinical-only, `1E2761` Ph2-reached). + +### (12) Competitive comparison table — Series B+ essential + +**Visual outcome.** 5–7 rows × 4–6 cols. Column 1 = competitor name (optional logo shape beside); rest = differentiators (speed / price / integrations / margin / coverage). **Last row = your company, fill highlighted** in an accent color (CADCFC / 97BC62); competitor rows gray. Every Series B+ deck needs this (SaaS: Datadog / New Relic / Splunk; Bio: Kite / Novartis / BMS). -# Slides 4-5: Team (C.11) + Problem/Solution (C.7) -- use patterns from creating.md -# Slide 6: Closing (C.12) -- gradient matching slide 1, fade transition +```bash +SLIDE=13 +officecli add "$FILE" / --type slide --prop layout=blank --prop background=FFFFFF +officecli add "$FILE" "/slide[$SLIDE]" --type shape --prop text="Competitive landscape" \ + --prop x=1.5cm --prop y=1.2cm --prop width=30.87cm --prop height=2cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 --prop fill=none +# Inline table via --prop data= (confirmed on v1.0.63; per-cell r#c# rejected). Single-quote the data value — '$15/host' would strip. +officecli add "$FILE" "/slide[$SLIDE]" --type table \ + --prop data='Competitor,Speed,Price,Integrations,Margin;Datadog,12 min,$15/host,680,75%;New Relic,18 min,$25/host,520,68%;Splunk,45 min,$45/GB,310,62%;You (Acme DevOps),90 sec,$8/host,1200,82%' \ + --prop style=medium1 --prop headerFill=1E2761 \ + --prop x=1.5cm --prop y=4cm --prop width=30.87cm --prop height=12cm +# Highlight your row: loop over /slide[$SLIDE]/table[1]/tr[5]/tc[1..5] and set cell fill to CADCFC. +``` + +**QA.** `officecli query "$FILE" 'table' --json | jq '.data.results | length'` ≥ 1. Row count ≥ 4 (you + ≥ 3 named competitors). Your row visually distinct via cell fill (Gate 5b visual check — table style alone does not highlight one row). + +## Numbers convention (pitch-specific) + +A terse convention table — **not a finance tutorial**. If you don't already know what these mean, pause the deck and ask the user for the values; don't guess. + +| Metric | Shape | Floor / convention | +|---|---|---| +| **TAM** | `$X.YB`, one methodology | Either top-down (analyst report) or bottom-up (count × ACV). Never both; never neither. | +| **SAM** | `$X.YB`, fraction of TAM you serve | Typically 15 – 30% of TAM for verticalized SaaS; higher for horizontal | +| **SOM** | `$X.YB` at year N | Realistic 5-yr share: 5 – 15% of SAM for early stage | +| **ARR** | MRR × 12. NOT revenue. | SaaS only; contracts on books, net of churn | +| **MRR** | Monthly recurring | ARR / 12; do not confuse with monthly revenue | +| **NRR (Net Revenue Retention)** | %, trailing 12 mo | VC floor: > 100% acceptable, > 115% strong, > 130% exceptional | +| **CAC** | $ fully-loaded | Sales + marketing spend / new logos acquired | +| **LTV** | $ | ARPU × gross margin × (1 / churn rate) | +| **LTV:CAC** | ratio | VC floor: 3x OK, > 4x strong, > 5x exceptional | +| **CAC payback** | months | VC floor: < 18 mo OK, < 12 mo strong | +| **Gross margin** | % | SaaS floor 70%, strong 80%+; marketplace 15-40%; hardware 30-50% | +| **Burn / runway** | $/month + months | Gross burn vs net burn — label which; runway to specific milestone | +| **Use of Funds** | 4-bucket pie | Engineering / Go-to-Market / G&A / Reserve — see Ask slide recipe | + +**Rule.** Every number on a deck carries a unit. `18%` or `18M` alone is ambiguous — write `$18M ARR` / `18% NRR growth`. `TBD`, `coming soon`, `(fill in)`, `lorem`, `xxxx` in numeric slots = immediate VC disqualification. Gate 6 greps these below. + +## VC ship-check (6 red flags / positive signals) + +What the VC reads in the first 30 seconds. Six one-line conditions — every "FAIL" below is an instant round-killer; fix before delivering. + +| # | Red flag (FAIL if present) | Positive signal (shipwise) | +|---|---|---| +| 1 | Cover without round + amount + date | `Company · tagline · Series X · $YM · Date` in 4 lines | +| 2 | TAM > $100B without a cited source / methodology | TAM clearly labeled bottom-up OR top-down with a visible 2024+ source | +| 3 | Traction chart y-axis does not start at 0 (hockey-stick lie) | Line chart `axismin=0`; growth shape honest | +| 4 | Team slide: headshots + names only, no prior companies | Every member: prior company + role + 1 achievement metric | +| 5 | Ask slide missing Use-of-Funds breakdown | `$XM` hero + 4-bucket pie (Eng / GTM / G&A / Reserve) + runway + next milestone | +| 6 | `TBD` / `lorem` / `xxxx` / `{{...}}` / `(fill in)` anywhere | `view text` clean — zero placeholder tokens | + +**Common Series-specific failures.** +- **Series A specific** — bottom-up TAM calculated from a fictional enterprise-count × ACV (no reference customers to anchor the count); `CAC / LTV` shown with < 12 months of data (statistically meaningless). +- **Series B specific** — no unit-econ slide at all; CAC payback > 24 months without a "we're pre-scale, here's the plan" narrative; logo wall < 8 customers. +- **Series C specific** — no moat / defensibility slide; revenue growth shown without margin trajectory; international expansion stated but no specific launch plan / hires. + +The Delivery Gate 6 block below executes checks 1–6 above via grep + query. Gate 5b fresh-eyes covers the visual judgments (hockey stick, team credibility) that grep can't see. + +## Traction triple-pattern (ARR + milestones + logos) -# Close and QA -officecli close pitch.pptx -officecli validate pitch.pptx +For Series B+, traction often spans 2 slides: one for the chart + callout (recipe 7 above), one for **milestone timeline + logo wall**. Timeline = 4-6 horizontal dates with one-line events. Logo wall = 12-20 customer logos in a 4×N or 5×N grid, muted monochrome so no single brand dominates. + +```bash +# Milestone timeline: 5 dates as circles on a horizontal line at y=8cm. +# Use pptx shapes (ellipse preset) + connectors (shape=straight) between them. +# Each milestone = ellipse at y=8cm + date label above + event description below. +# → See pptx v2 Recipe (d) row 9 (Roadmap timeline) for the canonical pattern. + +# Logo wall: pictures in a 5×N grid. Typical spacing: logo width = 5cm, height = 2cm, gap = 0.4cm. +# grid math for 5 logos across, 1.5cm edge margin: usable = 33.87 − 3 − 4·0.4 = 29.27, col = 5.85cm +# (use 5cm logo width centered in each 5.85cm column) ``` ---- +**QA.** Logo wall should have ≥ 8 logos for Series B+, ≥ 4 for Series A. Fewer = "lighter than it looks"; more than 20 = pixel noise. -## QA Checklist +## QA — Delivery Gate (executable) -Run before every delivery. See [creating.md](creating.md) Section G for the full checklist. +**Assume there are problems.** First render is almost never correct. Pitch decks fail at two layers: **structural** (schema, token leaks — caught by pptx v2 Gates 1–3) and **narrative** (wrong stage, missing unit econ, TAM unsourced — the checks that make pptx v2 Gate 5b + Gate 6 indispensable). Every check must print its success message. -1. `officecli validate` = 0 errors. **Exception:** slides with cell merge may report schema warnings that are cosmetic (PowerPoint renders correctly). -2. Every chart is editable (click -> Edit Data appears in PowerPoint) -3. Every table is a native table object (click -> table editing mode) -4. Speaker notes on all content slides (exclude title and closing), >= 2 sentences each -5. No two consecutive slides share the same layout structure -6. **Font consistency:** every shape in the deck uses only the declared font pairing (Georgia + Calibri). No shape may use an undeclared font (Hard Rule H-FONT). -7. **Palette-only colors:** scan all `fill` and `color` values — no bare hex outside the declared palette block. No red/green/gray introduced for status indicators (Hard Rule H-PALETTE). +### Gates 1–5a — inherited from pptx v2 verbatim ---- +→ see pptx v2 §Delivery Gate L637-679. Copy-paste the full block: -## What This Skill Does NOT Do +- **Gate 1** — `validate` schema check (whitelist `ChartShapeProperties` warnings per C-P-2). +- **Gate 2** — token leak via `view text` grep (`$xxx$`, `{{...}}`, ``, `lorem`, `xxxx`, empty `()`/`[]`, `\$`/`\t`/`\n` literals). +- **Gate 3** — hyperlink `rPr` schema trap (C-P-1) — zero ``. +- **Gate 4** — slide-order sanity — cover first, dividers before sections, closing last. +- **Gate 5a** — dark-on-dark contrast — every fill in `{1E2761, 0A1628, 8B1A1A, 2C5F2D, 36454F}` must declare near-white textColor. **This includes charts rendered on that fill**: chart `title.textColor`, `legend.textColor`, axis text default to dark and read as invisible on dark backgrounds — set them explicitly, or place the chart on a light card inside the dark slide. -- No morph transitions, clone-slide, or `!!` naming -- No 3D effects, motion paths, or video embedding -- No template merge (`{{key}}` patterns) -- No custom SVG geometry or WordArt -- No reading/editing existing .pptx (use officecli-pptx) +Do not skip or reorder these five. Every pptx-layer defect caught by Gates 1–5a also fires on pitch decks. ---- +**Gate 2b — pitch-specific shell-strip signatures (MANDATORY).** Gate 2 misses `$35M` that zsh silently stripped to empty (no residue to grep). Run this after Gate 2: + +```bash +# $XXM stripped by zsh leaves bare " M ARR" / " M raised" / "Series [A-C] · M" patterns. +STRIP=$(officecli view "$FILE" text | grep -niE '(^|[^A-Za-z0-9])M (ARR|raised|Series|runway|round|raise)|Series [A-C] · M( |$)|runway · M|raised · M|raising ·? M') +[ -z "$STRIP" ] && echo "Gate 2b OK (no \$-strip signatures)" || { echo "REJECT Gate 2b (likely zsh \$-strip — re-issue with single quotes):"; echo "$STRIP"; exit 1; } +``` -## Known Issues - -See [creating.md](creating.md) Section H for the full list with workarounds. Key issues: - -| Issue | Impact | -|-------|--------| -| `view issues` reports "Slide has no title" for every slide | **Expected behavior — safe to ignore.** pitch-deck uses `layout=blank` which has no built-in PowerPoint title placeholder. This is not a bug. | -| `gap` ignored during chart `add` | Must apply via separate `set` command | -| Cell merge produces validation errors | PowerPoint renders correctly; note in delivery | -| Cell-level `color` on table cells causes validation errors | Use row-level `color` instead | -| Custom gradient stops (`@`) fail on slide backgrounds | Use 2-color or 3-color gradients only | -| Combo chart requires both `comboSplit=1` and `secondary=2` | Missing either renders incorrectly | -| Dual-axis scale mismatch makes smaller series invisible | **HARD RULE:** If ranges differ >10x, MUST split into two separate charts. See creating.md D.4 | -| Stat values wrap at 60pt in 7cm width | **HARD RULE:** Max 4 chars for `$X.YM` patterns (wide `$`+`.` glyphs); max 5 chars for other values. Use 44-48pt or C.2 (3-stat, 9cm) for longer | -| Doughnut chart `colors` parameter may not apply | CLI accepts without error but PowerPoint renders default colors. No workaround. Verify via screenshot | -| Empty table cell `c1=""` causes validation error | Use `c1=" "` (space character) instead of empty string | -| Connector arrows may not all render in batch | Add connectors in separate batch after shapes; **immediately screenshot to verify each connector is visible** (CLI reports success even when rendering fails); if still missing, add one at a time or use `--type shape --prop preset=rightArrow` as a reliable fallback | -| Empty series values (gaps) not supported | Use `0` for missing data points; produces zero-height bars | -| Background covers text | In batch, background shapes are added before text shapes. Adding bg shapes AFTER text results in bg covering text completely (higher z-order). Always: bg → text, never text → bg | +Fix: re-issue the offending `add`/`set` with single quotes around the text value (`--prop text='Series B · $35M'`, not double quotes). The same strip hits **chart series names / axis titles** (`--prop name="营收 ($M)"` → legend shows `营收 ()`): single-quote every chart prop carrying `$`. ---- +### Gate 5b — Visual audit via HTML preview (MANDATORY, NOT optional) + +Gates 1–5a are token-grep defenses. **They cannot see a rendered slide.** This step is the only visual-assembly check. Do not skip. + +Run `officecli view "$FILE" html` and Read the returned HTML. Walk every slide and answer, for EACH (inherits pptx v2 Gate 5b checklist; pitch-specific additions marked ⭐): + +- **overlap**: do any text shapes overlap each other or a chart? +- **dark-on-dark**: is any text on a fill where fill brightness < 30% AND text brightness < 80%? +- **divider overlap**: any giant decorative number (01/02/03 at 100pt+) colliding with the divider title text? +- **order sanity**: does the slide sequence match your stage-appropriate narrative outline? +- **missing arrowheads**: do flowchart/decision-tree connectors show direction, or plain lines? +- ⭐ **traction y-axis**: does every ARR / revenue / growth line chart start at 0 on the y-axis? (Not 80% of current — that is the hockey-stick lie.) +- ⭐ **team credibility**: does every team-slide card show a prior company or prior title? (Cards with just headshot + name = reject.) +- ⭐ **TAM / market number credibility**: is the TAM under $100B for a niche market, or if ≥ $100B, is a methodology source cited? (A claimed `$500B TAM` with no source is an auto-reject red flag.) +- ⭐ **Use-of-Funds pie**: does the ask slide carry a 4-bucket pie (Engineering / GTM / G&A / Reserve) or a 4-card row with %s? +- ⭐ **narrative completeness**: is the order cover → problem → solution → market → product → model → traction → team → financials → ask, or your stage-appropriate permutation from §Stage diagnosis? -## Help System +**Instruction.** Run `officecli view "$FILE" html` and Read the HTML. Walk every slide against the questions below. If rendering chart colors, animations, or zoom — those only show in the target viewer (PowerPoint / Keynote / WPS); ask the user to open `.pptx` directly for those runtime features. + +> For every slide: +> (a) Are slides in VC narrative order (cover → problem → solution → market → product → model → traction → team → financials → ask, with your stage's adjustments)? Flag any out-of-sequence. +> (b) Is every ARR / revenue / growth line chart y-axis anchored at 0? Flag hockey-stick visual lies. +> (c) Does the team slide carry prior-company credentials for each person? (Not just headshot + name.) +> (d) Does every TAM / SAM / SOM claim have a visible source or methodology? +> (e) Does the ask slide have a 4-bucket Use of Funds (Engineering / GTM / G&A / Reserve) and a specific next milestone + runway length? +> (f) Any text overlap, dark-on-dark, off-slide geometry, missing arrowheads, placeholder tokens (`TBD` / `lorem` / `{{...}}` / `xxxx` / empty `()`)? + +Report every instance with slide number. If ANY defect — REJECT; do not deliver until fixed. + +**Human preview (optional).** If you want the user to visually preview the deck, run `officecli watch "$FILE"` for a live preview the user can open at their own discretion, or have them open the `.pptx` directly in PowerPoint / WPS / Keynote. For final visual verification, open the file in the target presentation viewer. + +### Gate 6 — Pitch narrative sanity (executable) + +Pitch-specific checks that grep the deck for VC red flags. Every one is a token check — combine with Gate 5b's human read for full coverage. ```bash -officecli pptx set # All settable elements and properties -officecli pptx set shape # Shape properties in detail -officecli pptx add # All addable element types -officecli pptx view # All view modes +FILE="deck.pptx" + +# 6.1 — no TBD / lorem / placeholder tokens (stronger than Gate 2 — pitch-specific scope) +LEAK=$(officecli view "$FILE" text | grep -niE 'TBD|lorem|\(fill in\)|xxxx|coming soon|placeholder') +[ -z "$LEAK" ] && echo "Gate 6.1 OK (no placeholder tokens)" || { echo "REJECT Gate 6.1:"; echo "$LEAK"; exit 1; } + +# 6.2 — TAM / SAM / SOM presence (Series A+) +TAM_HIT=$(officecli query "$FILE" 'shape:contains("TAM")' --json | jq '.data.results | length') +[ "$TAM_HIT" -ge 1 ] && echo "Gate 6.2 OK (TAM slide present)" || echo "WARN Gate 6.2: no TAM mention — confirm stage is Seed / Bridge if intentional" + +# 6.3 — Unit econ presence (Series B+): CAC OR LTV OR payback +CAC_HIT=$(officecli query "$FILE" 'shape:contains("CAC")' --json | jq '.data.results | length') +LTV_HIT=$(officecli query "$FILE" 'shape:contains("LTV")' --json | jq '.data.results | length') +if [ "$CAC_HIT" -ge 1 ] || [ "$LTV_HIT" -ge 1 ]; then + echo "Gate 6.3 OK (unit econ surface)" +else + echo "WARN Gate 6.3: no CAC / LTV — confirm stage Seed/A if intentional, REJECT if Series B+" +fi + +# 6.4 — Use of Funds present on ask slide +UOF_HIT=$(officecli query "$FILE" 'shape:contains("Use of Funds")' --json | jq '.data.results | length') +[ "$UOF_HIT" -ge 1 ] && echo "Gate 6.4 OK (Use of Funds)" || { echo "REJECT Gate 6.4: ask slide missing Use of Funds"; exit 1; } + +# 6.5 — Team prior-company signal (at least one of ex- / former / prior / previously) +PRIOR_HIT=$(officecli view "$FILE" text | grep -ciE '\b(ex-|former|prior|previously)\b') +[ "$PRIOR_HIT" -ge 1 ] && echo "Gate 6.5 OK (team prior-company)" || { echo "REJECT Gate 6.5: team slide has no prior-company credentials"; exit 1; } + +# 6.6 — Traction chart y-axis anchored at 0 (at least one chart must set axismin=0, Series A+) +AXISMIN_HIT=$(officecli query "$FILE" 'chart' --json | jq '[.data.results[]? | select(.format.axisMin == "0" or .format.axisMin == 0 or .format.axismin == "0" or .format.axismin == 0)] | length') +[ "$AXISMIN_HIT" -ge 1 ] && echo "Gate 6.6 OK (traction chart axisMin=0)" || echo "WARN Gate 6.6: no chart sets axisMin=0 — confirm no ARR/revenue line chart, or add --prop axismin=0" + +echo "Delivery Gate 6 PASS (token + narrative checks) — proceed to Gate 5b fresh-eyes (MANDATORY)" ``` + +**Readback key note.** CLI accepts lowercase `axismin` as input (on `--prop axismin=0`) but emits camelCase `axisMin` in `query --json` on v1.0.63. The jq above accepts both for forward-compat. + +Gate 6 is a grep floor. Gate 5b is the visual ceiling. Ship only when both print PASS. + +### Honest limit + +`validate` catches schema errors, not fundraising errors. A deck passes `validate` with a `$500B TAM` on a $10M market, a team slide of four co-founders with no prior companies, a hockey stick y-axis at 80%, a pitch for a Series B round without unit econ, and an ask slide saying "we're raising some money". Gates 5b + 6 above exist because `validate` cannot catch any of this. + +## Known Issues & Pitfalls + +→ Base pitfalls (shell escape, `[last()]` in resident, connector `@name=` rejection C-P-6, picture alt two-step C-P-7, animation remove C-P-4, chart color normalization C-P-7): see pptx v2 §Known Issues & Pitfalls C-P-1..7. + +Pitch-specific: + +- **Stage misidentified.** Series A deck with 6 pages of CAC/LTV math = over-packaged. Series B deck missing unit econ = incomplete. If unsure, re-read §Stage diagnosis before building. +- **Hockey-stick y-axis.** If the line chart's y-axis doesn't start at 0, VCs read it as a visual lie within 2 seconds. Always `--prop axismin=0` on ARR / revenue / growth charts. Gate 6.6 checks this. +- **Team slide = portfolio.** Cards showing only {headshot + name + role} fail VC credibility. Every card needs a prior-company or prior-achievement line. Gate 6.5 checks this. +- **TAM without methodology.** A claimed number with no "top-down" or "bottom-up" source footnote = fabricated. Pick one methodology per deck; don't mix. +- **Use-of-Funds as 3-bucket or 5-bucket.** 4-bucket (Eng / GTM / G&A / Reserve) is convention; departing from it reads as sloppy. Gate 6.4 checks presence. +- **Pitch deck used for a board review / sales deck.** Narrative arc (problem → ask) makes board reviews awkward — route to pptx v2 Recipe (d) 10-slide instead. See §Reverse handoff above. +- **pptx v2 Recipe (d′) 20-slide is a starting point, not a formula.** It is stage-agnostic SaaS. Adjust for your stage + 赛道 via §Stage diagnosis and §赛道 arc templates — never ship (d′) unchanged for a non-SaaS Series A. + +## Help pointer + +When in doubt: `officecli help pptx`, `officecli help pptx `, `officecli help pptx --json`. Help is the authoritative schema; this skill is the decision guide for fundraising deltas on top of pptx v2. diff --git a/skills/officecli-pitch-deck/creating.md b/skills/officecli-pitch-deck/creating.md deleted file mode 100644 index 13e53e6be..000000000 --- a/skills/officecli-pitch-deck/creating.md +++ /dev/null @@ -1,705 +0,0 @@ - - - - -# Creating a Pitch Deck - -Complete guide for building professional pitch presentations from scratch. Follow the workflow: decide structure, set up, build slides pattern-by-pattern, polish, QA. - -For general pptx building blocks (shapes, pictures, rich text, animations, batch syntax), see [pptx creating.md](../officecli-pptx/creating.md). This document focuses on pitch-deck-specific patterns and recipes. - ---- - -## Section A: Decision Logic - -### A.1 Deck Type Selection - -| Deck Type | Slides | When to Use | Recommended Slide Sequence | -|-----------|--------|------------|---------------------------| -| Seed Pitch | 6 | Pre-seed/seed, < $1M, early metrics | Title, Problem+Solution, Market, Traction, Team, Ask | -| Product Launch | 8 | Feature announcement, product release | Title, Problem, Features, Before/After, Demo, Results, Pricing, CTA | -| Full Investor | 10-12 | Series A+, significant traction | Title, Problem, Solution, Market, Product, Traction, Business Model, Competitive, Roadmap, Team, Financials, Ask | -| Enterprise Sales | 10 | B2B, C-level audience, data-heavy | Title, Threat/Problem, Solution, Architecture, ROI, Case Study, Competitive, Impact, Timeline, Next Steps | - -These are starting points. **Respect the user's slide count** -- never pad to a longer deck. Adapt the sequence to the user's specific request. - -### A.2 Color Palette Reference - -Define palette as shell variables before building. All subsequent commands reference variables. - -**Professional Navy** (investor decks): -```bash -PRIMARY="0F2B46"; SECONDARY="1A73E8"; ACCENT1="34A853"; ACCENT2="F9AB00"; DARK="0A1628"; DARK2="0D1F35"; LIGHT="F0F4F8"; MUTED="64748B" -``` - -**Tech Purple** (product launches): -```bash -PRIMARY="6C2BD9"; SECONDARY="1DB954"; ACCENT1="FF6B35"; ACCENT2="00B4D8"; DARK="1A1A2E"; DARK2="12102A"; LIGHT="F8F7FF"; MUTED="64748B" -``` - -**Dark Premium** (enterprise sales): -```bash -PRIMARY="0D0D1A"; SECONDARY="00D4AA"; ACCENT1="FF4757"; ACCENT2="FFA502"; ACCENT3="2ED573"; DARK="0D0D1A"; DARK2="1A1A3E"; LIGHT_TEXT="E8E8E8"; MUTED="6B7B8D" -``` - -**Impact/Humanitarian** (nonprofit / social impact): -```bash -PRIMARY="2E7D32"; SECONDARY="1565C0"; ACCENT1="E65100"; DARK="1B5E20"; DARK2="0D3B6E"; LIGHT="FAFAFA"; TEXT="212121"; MUTED="64748B" -``` - -If the user provides specific colors, use those. If not, select the closest palette and adapt. - -> **Color Palette Discipline (Hard Rule H-PALETTE):** Once defined, use ONLY the named palette variables (`$PRIMARY`, `$SECONDARY`, `$ACCENT1`, `$ACCENT2`, etc.) throughout the entire deck. Do NOT introduce new accent colors mid-deck. If Slide 3 uses purple (`6C2BD9`) but that value was not declared in your initial palette block, purple is out-of-palette and must be removed or replaced with an existing palette variable. Every hex color value in the deck must map to a named palette variable. Consistency check: before delivery, scan all shape `fill` and `color` values — any bare hex not in your palette block is a violation. -> -> **Banned color introductions — common traps that WILL cause a FAIL:** -> - ❌ Red to indicate "bad/wrong/competitor weakness" — use `$DARK` or `$DARK2` instead -> - ❌ Green to indicate "good/correct/your advantage" — use `$PRIMARY` or `$ACCENT1` instead (unless green is already in your palette) -> - ❌ Gray (`888888`, `CCCCCC`, etc.) as a neutral or muted-text color — use a declared palette variable; add a `MUTED` variable to your palette block if you need a muted shade -> - ❌ Any color not present in your declared palette block, regardless of reason -> -> **Expressing contrast and status with palette colors only:** -> - Competitor / "without" column → darker fill from palette (e.g., `$DARK`, `$DARK2`, or a low-opacity `$PRIMARY`) -> - Your product / "with" column → primary or accent fill (e.g., `$PRIMARY`, `$SECONDARY`) -> - Comparison table checkmarks vs. crosses → use the same column text color; do NOT switch to red/green -> - If you need a muted color for sub-labels, declare it upfront: `MUTED="64748B"` and reference `$MUTED` everywhere - -### A.3 Font Pairing - -| Element | Font | Size | -|---------|------|------| -| Slide title | Georgia, bold | 32-44pt | -| Section header | Georgia or Calibri, bold | 18-24pt | -| Body text | Calibri | **16pt minimum** (Hard Rule H-FONT — no exceptions) | -| Stat number | Georgia, bold | 36-64pt | -| Stat label | Calibri | **16pt minimum** (Hard Rule H-FONT — stat sub-labels are body text) | -| Chart axis / legend | Calibri | 10pt (chart-internal exception — allowed) | -| Table cell text | Calibri | 11-12pt (table-internal exception — allowed) | -| Caption/footer | Calibri | 12pt minimum | - -> **Font consistency (Hard Rule H-FONT):** The pairing above (Georgia + Calibri) applies to EVERY text shape in the deck without exception — including connector labels, footer shapes, page numbers, and any small caption. Do NOT use any other font (Arial, Helvetica, Times New Roman, etc.) anywhere in the deck. Every `add` command must include an explicit `"font"` value; never rely on PowerPoint defaults. Before delivery, scan every shape in your script and confirm only Georgia and Calibri appear. - ---- - -## Section B: Setup - -### B.1 Create File and Set Metadata - -```bash -officecli create deck.pptx -officecli set deck.pptx / --prop title="Deck Title" --prop author="Author Name" -``` - -### B.2 Define Color Palette - -```bash -# Example: Professional Navy -PRIMARY="0F2B46" -SECONDARY="1A73E8" -ACCENT1="34A853" -ACCENT2="F9AB00" -DARK="0A1628" -LIGHT="F0F4F8" -``` - -### B.3 Resident Mode (Optional, 3+ Commands) - -```bash -officecli open deck.pptx # Keep in memory -# ... build slides ... -officecli close deck.pptx # Save and release -``` - ---- - -## Section C: Slide Patterns - -Each pattern includes: visual description, positioning table, and batch template skeleton. Replace placeholder text, colors, and data with scenario content. **Add speaker notes after each slide is built.** **Never use the same pattern on two consecutive slides.** - -> **Vertical centering:** When a slide has fewer elements than the pattern maximum, adjust y-positions downward by 2-3cm to center the visual weight. The positions in these patterns assume maximum content. - -### C.1 Title / Cover (Dark Gradient) - -3-4 text shapes on gradient background. Slide 1 in all decks. Transition: `fade`. - -| Element | X | Y | Width | Height | Font/Size | -|---------|---|---|-------|--------|-----------| -| Title | 2cm | 5cm | 29.87cm | 4cm | Georgia bold 44pt | -| Tagline | 2cm | 10cm | 29.87cm | 2cm | Calibri 20pt | -| Footer | 2cm | 13cm | 29.87cm | 1.5cm | Calibri 12pt | - -Background: `"$DARK-$DARK2-180"` (linear gradient, angle 180 = top-to-bottom). `DARK2` is the second dark shade defined in the palette (e.g., `0D1F35` for Professional Navy, `12102A` for Tech Purple, `1A1A3E` for Dark Premium). See SKILL.md Quick Start for full batch template. - -### C.2 Stat Callout Row (3-Stat) - -Title + 3 number/label pairs. Numbers: Georgia bold 64pt. Labels: Calibri **16pt minimum** (Hard Rule H-FONT), muted. - -| Stat | Number X | Label X | Width | Number Y | Label Y | -|------|----------|---------|-------|----------|---------| -| 1 | 2cm | 2cm | 9cm | 5cm | 9.5cm | -| 2 | 12.5cm | 12.5cm | 9cm | 5cm | 9.5cm | -| 3 | 23cm | 23cm | 9cm | 5cm | 9.5cm | - -Title at y=1cm, height 3cm. Number height 4cm, label height 2cm. - -### C.3 Stat Callout Row (4-Stat) - -Same as C.2 but 4 columns. Numbers: Georgia bold 60pt. - -Stat X positions: 1.5cm, 9.5cm, 17.5cm, 25.5cm. Width: 7cm each. - -> **HARD RULE: At 60pt in 7cm width, stat values must be 4 characters or fewer for dollar-amount patterns containing both `$` and `.` (e.g., `$9.4M` is 5 chars but the wide `$` and `.` glyphs in Georgia bold make it effectively 6 — it WILL wrap). Safe dollar patterns: `$9M`, `$96B`, `$4K` (3-4 chars). For non-dollar values, 5 characters is the limit (e.g., "340%", "4.2m", "12.3" are safe). Values of 6+ characters (e.g., "197min", "3 Days") WILL wrap to 2 lines and destroy the stat callout. For longer values: (a) reduce font to 44-48pt, (b) abbreviate (e.g., "197m" instead of "197min", "$9M" instead of "$9.4M"), or (c) use the wider 3-stat layout (C.2, 9cm per stat). Single tokens only — no spaces.** - -### C.4 Chart + Context (Chart Left, Stats Right) - -Chart on left 55%, stat callouts stacked on right. Post-batch: `officecli set "/slide[N]/chart[1]" --prop gap=80` for column/bar. - -| Element | X | Y | Width | Height | -|---------|---|---|-------|--------| -| Title | 2cm | 1cm | 29.87cm | 3cm | -| Chart | 2cm | 4cm | 17cm | 13cm | -| Stats | 21cm | 4cm+ | 11cm | 2.5cm number + 1.5cm label | - -Stat spacing: ~3.7cm per pair. For 5 stats, use size=44pt. Stat sub-labels: **16pt minimum** (Hard Rule H-FONT). - -### C.5 Icon-in-Circle Grid (3-Row Vertical) - -3 rows at y=4.5cm, 8.5cm, 12.5cm. Each: ellipse 2.5x2.5cm at x=2cm + text overlay (same pos) + label at x=5.5cm (bold 18pt, width 25cm) + description below (**16pt minimum**, muted). 13 elements total. - -### C.5b 2×2 Feature Grid (4-Item) - -4 feature cards in a 2-column × 2-row grid. Use when you have exactly 4 parallel items (product features, service types, pillars, etc.). Each card: rounded-rect background + icon/emoji ellipse + title + description. - -| Element | X | Y | Width | Height | Notes | -|---------|---|---|-------|--------|-------| -| Card 1 (top-left) bg | 1.5cm | 4cm | 14.5cm | 7cm | roundRect, fill=card color | -| Card 2 (top-right) bg | 17.5cm | 4cm | 14.5cm | 7cm | | -| Card 3 (bottom-left) bg | 1.5cm | 12cm | 14.5cm | 7cm | | -| Card 4 (bottom-right) bg | 17.5cm | 12cm | 14.5cm | 7cm | | -| Icon ellipse (each card) | card_x+0.5cm | card_y+0.5cm | 2cm | 2cm | centered on left | -| Title (each card) | card_x+3.2cm | card_y+0.6cm | 10.5cm | 1.8cm | bold 16pt | -| Body (each card) | card_x+0.5cm | card_y+3cm | 13cm | 3.5cm | **16pt** (Hard Rule H-FONT) | - -**Slide-level title:** Georgia bold 32pt at y=1cm, height=2.5cm. - -> **Z-Order rule:** The batch skeleton below already follows the correct order: each card's background shape appears immediately before that card's text shapes. Do NOT reorder — moving any `roundRect` bg after its text shapes will cause the background to cover the text. - -**Batch skeleton:** - -```bash -cat <<'EOF' | officecli batch deck.pptx -[ - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Four Pillars of Our Platform","x":"2cm","y":"1cm","width":"29.87cm","height":"2.5cm","font":"Georgia","size":"32","bold":"true","color":"$PRIMARY","align":"left","fill":"none"}}, - - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"roundRect","x":"1.5cm","y":"4cm","width":"14.5cm","height":"7cm","fill":"F0F4F8","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"ellipse","x":"2cm","y":"4.5cm","width":"2cm","height":"2cm","fill":"$SECONDARY","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Feature One","x":"4.7cm","y":"4.6cm","width":"10.5cm","height":"1.8cm","font":"Calibri","size":"16","bold":"true","color":"$PRIMARY","align":"left","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Brief description of feature one and its core benefit.","x":"2cm","y":"7cm","width":"13cm","height":"3.5cm","font":"Calibri","size":"16","color":"$MUTED_HEX","align":"left","fill":"none"}}, - - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"roundRect","x":"17.5cm","y":"4cm","width":"14.5cm","height":"7cm","fill":"F0F4F8","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"ellipse","x":"18cm","y":"4.5cm","width":"2cm","height":"2cm","fill":"$ACCENT1","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Feature Two","x":"20.7cm","y":"4.6cm","width":"10.5cm","height":"1.8cm","font":"Calibri","size":"16","bold":"true","color":"$PRIMARY","align":"left","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Brief description of feature two and its core benefit.","x":"18cm","y":"7cm","width":"13cm","height":"3.5cm","font":"Calibri","size":"16","color":"$MUTED_HEX","align":"left","fill":"none"}}, - - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"roundRect","x":"1.5cm","y":"12cm","width":"14.5cm","height":"7cm","fill":"F0F4F8","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"ellipse","x":"2cm","y":"12.5cm","width":"2cm","height":"2cm","fill":"$ACCENT2","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Feature Three","x":"4.7cm","y":"12.6cm","width":"10.5cm","height":"1.8cm","font":"Calibri","size":"16","bold":"true","color":"$PRIMARY","align":"left","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Brief description of feature three and its core benefit.","x":"2cm","y":"15cm","width":"13cm","height":"3.5cm","font":"Calibri","size":"16","color":"$MUTED_HEX","align":"left","fill":"none"}}, - - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"roundRect","x":"17.5cm","y":"12cm","width":"14.5cm","height":"7cm","fill":"F0F4F8","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"ellipse","x":"18cm","y":"12.5cm","width":"2cm","height":"2cm","fill":"$PRIMARY","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Feature Four","x":"20.7cm","y":"12.6cm","width":"10.5cm","height":"1.8cm","font":"Calibri","size":"16","bold":"true","color":"$PRIMARY","align":"left","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Brief description of feature four and its core benefit.","x":"18cm","y":"15cm","width":"13cm","height":"3.5cm","font":"Calibri","size":"16","color":"$MUTED_HEX","align":"left","fill":"none"}} -] -EOF -``` - -> **`$MUTED_HEX`**: Replace with the literal hex value of your declared `$MUTED` palette variable (e.g., `64748B`). Variables are not expanded inside heredoc single-quote blocks — you must use the bare hex. Per H-PALETTE, all colors must trace back to a named palette variable; never use `444444` or any undeclared hex. - -> For dark-background slides, change card fill to a lighter dark shade (e.g., `fill="1A2540"`) and text color to `FFFFFF`/`E8E8E8`. - -### C.6 Icon-in-Circle Grid (5-Across Horizontal) - -5 icons at x=1.5, 7.5, 13.5, 19.5, 25.5cm. Circle 2x2cm at y=5cm. Label below at y+2.5cm, desc at y+4cm, width 5cm each. 21 elements -- use batch. Post-placement: `distribute=horizontal` for precision. - -### C.7 Two-Column Split - -Problem+solution, before/after. Card backgrounds (roundRect) first, then text on top. - -> **CRITICAL — Shape Z-Order in Batch:** -> Background card shapes MUST be added BEFORE text shapes in every batch. -> Shapes added later in batch appear on top (higher z-order) and will cover earlier shapes. -> **Required order**: `[bg card 1] → [text on card 1] → [bg card 2] → [text on card 2]` -> If you add background shapes AFTER text shapes, the cards will cover all text completely. - -| Element | X | Y | Width | Height | -|---------|---|---|-------|--------| -| Left card bg | 2cm | 4.5cm | 14.5cm | 13cm | -| Left header | 3cm | 5cm | 12.5cm | 2cm | -| Left body | 3cm | 7.5cm | 12.5cm | 9cm | -| Right card bg | 17.5cm | 4.5cm | 14.5cm | 13cm | -| Right header | 18.5cm | 5cm | 12.5cm | 2cm | -| Right body | 18.5cm | 7.5cm | 12.5cm | 9cm | - -> **Color guidance for before/after contrast:** The two columns MUST be visually distinct. Use contrasting fills — not two shades of the same dark color. -> - "Without" (problem) column → dark red/orange tint (e.g., `fill="3D1010"` or `fill="2D1505"` for dark themes; `fill="FFF0F0"` for light themes). Header accent: `ACCENT1` red or orange. -> - "With" (solution) column → dark green/teal tint (e.g., `fill="0D2D1A"` for dark themes; `fill="F0FFF4"` for light themes). Header accent: `ACCENT1` green or `SECONDARY`. -> - Test via screenshot: both columns must be immediately distinguishable at a glance. - -### C.8 Comparison Table (Full-Width) - -Title (2cm, 1cm) + table (2cm, 4.5cm, 29.87cm x 13cm). See Section E for construction. - -### C.9 Flow Diagram (Connectors) - -**3-node** (most common -- e.g., "Install → Monitor → Save"): - -| Element | X | Y | Width | Height | -|---------|---|---|-------|--------| -| Node 1 (left) | 1cm | 9cm | 7cm | 4cm | -| Arrow 1→2 | 8.5cm | 10.5cm | 3cm | 1cm | -| Node 2 (center) | 12cm | 9cm | 7cm | 4cm | -| Arrow 2→3 | 19.5cm | 10.5cm | 3cm | 1cm | -| Node 3 (right) | 23cm | 9cm | 7cm | 4cm | - -(slide width = 33.87cm; nodes are vertically centered at y=9cm) - -**4-node**: x=1, 8.5, 16, 23.5cm. Width 6cm, y=8cm, height 3cm. -**5-node**: x=0.5, 7, 13.5, 20, 26.5cm. Width 5.5cm. - -Title = shape[1]. Flow shapes start at shape[2]. Add all shapes BEFORE connectors. Connectors work within the same batch. - -> **Node text color rule (H-11 enforcement):** -> Node cards on dark-background slides use dark fills (e.g., `$PRIMARY`, `$DARK2`). All text on these nodes — both the title text and the description text below the node — MUST use light colors. -> - Node title text: `"color":"FFFFFF"` or `"color":"$LIGHT_TEXT"` -> - Node description text: `"color":"E8E8E8"` or `"color":"FFFFFF"` — **never use `MUTED` (6B7B8D) on dark node backgrounds; it is too dark to read** -> - Only use `MUTED` or `"color":"333333"` when the node card has a **light** fill. - -> **Connector reliability:** Add connectors in a separate batch AFTER all node shapes are created. Verify each connector is visible. If batch connectors are unreliable, add them one at a time with individual `add` commands. - -> **MANDATORY CONNECTOR VERIFICATION (PNG only):** -> Do NOT use `officecli view slides.pptx svg` to verify connectors — SVG rendering may disagree with LibreOffice. -> You MUST verify connectors via the authoritative screenshot pipeline: -> `screenshot.mjs` → PDF → PNG -> -> **After generating the PNG, verify ALL of the following — both checks are required:** -> 1. **Every connector is visible** (not missing or transparent) -> 2. **No connector overlaps or obscures any text shape** — zoom into each node label in the PNG and confirm the connector line/arrow does not cross over any text content -> -> If a connector is missing in the PNG screenshot: -> - Fall back to a visible `rightArrow` shape instead of connector: -> `{"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"rightArrow","x":"...","y":"...","width":"1.5cm","height":"0.8cm","fill":"ACCENT_COLOR","line":"none"}}` -> - rightArrow shapes render reliably in all viewers. -> -> If a connector overlaps text in the PNG screenshot: -> - Add 0.5cm of padding to the connector's start or end point by adjusting `startX`/`startY`/`endX`/`endY` to move the endpoint away from the text region; OR -> - Use `--prop zorder=back` on the connector to send it behind the text shape; OR -> - Replace with a `rightArrow` fallback (see above) positioned to avoid the text box. - -> **Coordinate tip for precise connectors:** Before specifying `startX/startY/endX/endY`, confirm adjacent node positions with: -> ```bash -> officecli get deck.pptx "/slide[N]" --depth 1 --json -> ``` -> Use the returned `x`, `y`, `width`, `height` values to calculate exact connector endpoints (e.g., `startX = nodeX + nodeWidth`, `startY = nodeY + nodeHeight/2`). - -### C.10 Timeline / Roadmap (Horizontal) - -Spine: connector at y=10cm, full width. 4 milestones (3x3cm circles) at x=4, 12, 20, 28cm, y=8.5cm. Alternating above/below labels: odd at y=5.5cm/7cm, even at y=12cm/13.5cm. ~19 elements -- single batch (reliable). - -Add the spine connector first: -```bash -officecli add deck.pptx "/slide[N]" --type connector --prop preset=straight --prop startX=2cm --prop startY=10cm --prop endX=32cm --prop endY=10cm --prop line=$SECONDARY --prop lineWidth=2pt -``` - -### C.11 Avatar Grid - -**2-person** (centered, symmetric): - -| Element | X | Y | Width | Height | -|---------|---|---|-------|--------| -| Person 1 avatar | 7cm | 6cm | 8cm | 5cm | -| Person 2 avatar | 18cm | 6cm | 8cm | 5cm | - -Name (bold 16pt) + role (16pt) centered below each avatar; use same X and width as avatar. - -**2x2**: circles at (6cm, 4.5cm), (20cm, 4.5cm), (6cm, 12.5cm), (20cm, 12.5cm). Size 3.5x3.5cm. -**3-row**: circles at x=4.5, 15.5, 26.5cm, y=5cm. Size 3x3cm. - -Each member: ellipse + initials overlay + name (bold 16pt) + role (16pt) = 4 elements. Name width 6cm centered. - -> **CRITICAL — 3-person layout: use the batch template below exactly. Each person occupies a fixed column; bio text is pinned to an absolute y-coordinate to prevent overlap. Do NOT shrink height below the values shown.** - -**3-person complete batch example** (x=1.5, 12.0, 22.5cm columns; slide width 33.87cm): - -```bash -cat <<'EOF' | officecli batch deck.pptx -[ - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Meet the Team","x":"2cm","y":"1cm","width":"29.87cm","height":"3cm","font":"Georgia","size":"36","bold":"true","color":"$PRIMARY","align":"left","fill":"none"}}, - - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"ellipse","x":"2.5cm","y":"4.5cm","width":"3cm","height":"3cm","fill":"$SECONDARY","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"AB","x":"2.5cm","y":"4.5cm","width":"3cm","height":"3cm","font":"Georgia","size":"22","bold":"true","color":"FFFFFF","align":"center","valign":"middle","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Alice Brown","x":"1.5cm","y":"8cm","width":"5cm","height":"1.5cm","font":"Calibri","size":"16","bold":"true","color":"$PRIMARY","align":"center","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"CEO & Co-Founder","x":"1.5cm","y":"9.5cm","width":"5cm","height":"1.5cm","font":"Calibri","size":"16","color":"64748B","align":"center","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"10 yrs enterprise SaaS. Ex-Salesforce. Led $50M ARR growth.","x":"1.5cm","y":"11.2cm","width":"9cm","height":"5cm","font":"Calibri","size":"16","color":"$PRIMARY","align":"left","fill":"none"}}, - - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"ellipse","x":"13.5cm","y":"4.5cm","width":"3cm","height":"3cm","fill":"$ACCENT1","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"CD","x":"13.5cm","y":"4.5cm","width":"3cm","height":"3cm","font":"Georgia","size":"22","bold":"true","color":"FFFFFF","align":"center","valign":"middle","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Carlos Diaz","x":"12.5cm","y":"8cm","width":"5cm","height":"1.5cm","font":"Calibri","size":"16","bold":"true","color":"$PRIMARY","align":"center","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"CTO & Co-Founder","x":"12.5cm","y":"9.5cm","width":"5cm","height":"1.5cm","font":"Calibri","size":"16","color":"64748B","align":"center","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"ML infra at scale. Ex-Google Brain. 3 patents in NLP.","x":"12.5cm","y":"11.2cm","width":"9cm","height":"5cm","font":"Calibri","size":"16","color":"$PRIMARY","align":"left","fill":"none"}}, - - {"command":"add","parent":"/slide[N]","type":"shape","props":{"preset":"ellipse","x":"24.5cm","y":"4.5cm","width":"3cm","height":"3cm","fill":"$ACCENT2","line":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"EF","x":"24.5cm","y":"4.5cm","width":"3cm","height":"3cm","font":"Georgia","size":"22","bold":"true","color":"FFFFFF","align":"center","valign":"middle","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"Eva Fischer","x":"23.5cm","y":"8cm","width":"5cm","height":"1.5cm","font":"Calibri","size":"16","bold":"true","color":"$PRIMARY","align":"center","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"VP Sales","x":"23.5cm","y":"9.5cm","width":"5cm","height":"1.5cm","font":"Calibri","size":"16","color":"64748B","align":"center","fill":"none"}}, - {"command":"add","parent":"/slide[N]","type":"shape","props":{"text":"$30M pipeline closed in 18 months. Ex-HubSpot, AWS.","x":"23.5cm","y":"11.2cm","width":"9cm","height":"5cm","font":"Calibri","size":"16","color":"$PRIMARY","align":"left","fill":"none"}} -] -EOF -``` - -> **No-overlap rule:** Each person's column is 11cm wide (gap between columns ≥ 1cm). Avatar at y=4.5cm, name at y=8cm, role at y=9.2cm, bio at y=10.5cm — all three persons share the same y-coordinates. Bio text box height=5.5cm; keep bio text to 2-3 short sentences to avoid overflow. - -### C.12 Closing / CTA (Dark Gradient) - -Mirror slide 1 gradient. Main CTA (44-48pt) at y=4cm, details (16pt) at y=9cm, contact (12pt) at y=15cm. Transition: `fade`. - ---- - -## Section D: Charts - -### D.0 Chart Styling Baseline (Mandatory) - -Apply to EVERY chart. No default PowerPoint styling is acceptable. - -**Light theme** (white/light backgrounds): -``` -plotFill=none, chartFill=none, gridlines="E2E8F0:0.5", -axisFont="10:64748B:Calibri", legendFont="10:64748B:Calibri", -series.outline="FFFFFF-0.5", legend=bottom -``` - -**Dark theme** (dark backgrounds): -``` -plotFill=none, chartFill=none, gridlines="2A2A4A:0.5", -axisFont="10:6B7B8D:Calibri", legendFont="10:6B7B8D:Calibri", -series.outline="FFFFFF-0.5", legend=bottom -``` - -> **`gap` MUST be set via a separate `set` command after chart creation. It is ignored during `add`.** - -### D.1 Column Chart (Single Series) - -```bash -officecli add deck.pptx "/slide[N]" --type chart \ - --prop chartType=column --prop title="Quarterly ARR ($K)" \ - --prop categories="Q1,Q2,Q3,Q4" --prop series1="ARR:1400,1720,2060,2400" \ - --prop x=2cm --prop y=4cm --prop width=18cm --prop height=13cm \ - --prop colors=1A73E8 --prop plotFill=none --prop chartFill=none \ - --prop "gridlines=E2E8F0:0.5" --prop dataLabels=value --prop labelPos=outsideEnd \ - --prop "labelFont=10:64748B:false" --prop "axisFont=10:64748B:Calibri" \ - --prop "legendFont=10:64748B:Calibri" --prop "series.outline=FFFFFF-0.5" --prop legend=bottom -officecli set deck.pptx "/slide[N]/chart[1]" --prop gap=80 -``` - -All subsequent chart recipes include the same D.0 baseline props. Only type-specific differences are noted. - -### D.2 Multi-Series Column - -Same as D.1 but with `series1` + `series2` and two colors. Post-creation `gap=80` required. - -> **Empty series values are not supported.** Use `0` for missing data points: `series1="Actual:1400,1720,2060,2400,0,0,0,0"` / `series2="Projected:0,0,0,0,3000,3800,4700,5800"`. Note: this produces zero-height bars instead of gaps. For true gap visualization, use separate charts. - -### D.3 Doughnut - -`chartType=doughnut`, `data="Rating:42,38,14,4,2"`, `dataLabels=percent`, `legend=right`. No `gap` step needed. Use 5 colors from strong to muted. - -> **Known limitation: The `colors` parameter may not apply to doughnut charts.** PowerPoint may render doughnut segments in default Office colors instead of specified colors. This is a CLI limitation. Verify colors via screenshot after creation. If colors are wrong, there is currently no workaround. - -### D.4 Combo Chart (Dual-Axis) -- HIGHEST RISK - -> Both `comboSplit=1` AND `secondary=2` are REQUIRED. Missing either causes incorrect rendering. - -> **HARD RULE: Before creating a combo chart, verify that both series use similar ranges (e.g., both 0-100 or both 0-10). If ranges differ by more than 10x (e.g., $M values vs % values), DO NOT use a combo chart. Instead, create two separate side-by-side charts: a bar chart for the absolute values and a line chart for the percentages. Place them using pattern C.4 (chart left + stats right) or use two 50% width charts.** - -> **⚠️ WARNING — Dual-series magnitude mismatch:** If one series uses values in the millions (e.g., Revenue $M: 2, 5, 8, 12) and the other uses counts or percentages (e.g., Customer Count: 200, 450, 800, 1200), the smaller-magnitude series will be rendered nearly invisible even with `secondary=2`. Examples of dangerous combinations: Revenue ($M) + Customer Count, ARR ($M) + Headcount, Market Share (%) + Absolute Sales. **Check the ratio before building:** if `max(series1) / max(series2) > 10` or `max(series2) / max(series1) > 10`, you MUST split into two separate charts. After building a dual-series chart, always run a screenshot to verify both series are visually legible. - -**Single combo chart (ONLY when both series have similar ranges):** - -```bash -officecli add deck.pptx "/slide[N]" --type chart \ - --prop chartType=combo --prop categories="Year 0,Year 1,Year 2,Year 3" \ - --prop "series1=Cost Savings ($M):-0.8,1.2,2.8,3.5" \ - --prop "series2=Cumulative ROI (%):-100,50,250,340" \ - --prop comboSplit=1 --prop secondary=2 \ - --prop colors=00D4AA,FFA502 \ - --prop plotFill=none --prop chartFill=none --prop "gridlines=2A2A4A:0.5" \ - --prop "axisFont=10:6B7B8D:Calibri" --prop "legendFont=10:6B7B8D:Calibri" \ - --prop "series.outline=FFFFFF-0.5" --prop legend=bottom \ - --prop x=2cm --prop y=4cm --prop width=29cm --prop height=13cm -officecli set deck.pptx "/slide[N]/chart[1]" --prop gap=80 -``` - -`comboSplit=1` = first series as bars, rest as lines. `secondary=2` = series 2 on secondary Y-axis. - -**Two-Chart Alternative (REQUIRED when ranges differ by >10x):** - -Use two separate charts side by side instead of a single combo chart. This avoids the dual-axis scale mismatch that makes the smaller series invisible. - -```bash -# Left chart: bar chart for absolute values (55% width) -officecli add deck.pptx "/slide[N]" --type chart \ - --prop chartType=column --prop title="Cost Savings ($M)" \ - --prop categories="Year 0,Year 1,Year 2,Year 3" \ - --prop "series1=Cost Savings:-0.8,1.2,2.8,3.5" \ - --prop colors=00D4AA \ - --prop plotFill=none --prop chartFill=none --prop "gridlines=2A2A4A:0.5" \ - --prop "axisFont=10:6B7B8D:Calibri" --prop "legendFont=10:6B7B8D:Calibri" \ - --prop "series.outline=FFFFFF-0.5" --prop legend=bottom \ - --prop x=2cm --prop y=4cm --prop width=15cm --prop height=13cm -officecli set deck.pptx "/slide[N]/chart[1]" --prop gap=80 - -# Right chart: line chart for percentages (45% width) -officecli add deck.pptx "/slide[N]" --type chart \ - --prop chartType=line --prop title="Cumulative ROI (%)" \ - --prop categories="Year 0,Year 1,Year 2,Year 3" \ - --prop "series1=ROI:-100,50,250,340" \ - --prop colors=FFA502 \ - --prop plotFill=none --prop chartFill=none --prop "gridlines=2A2A4A:0.5" \ - --prop "axisFont=10:6B7B8D:Calibri" --prop "legendFont=10:6B7B8D:Calibri" \ - --prop "series.outline=FFFFFF-0.5" --prop legend=bottom \ - --prop x=18cm --prop y=4cm --prop width=14cm --prop height=13cm -``` - -### D.5 Radar Chart - -`chartType=radar`, 3 series via `series1`/`series2`/`series3`. Position: x=4cm, width=26cm, height=14cm. Include D.0 dark-theme baseline. No `gap` step needed. - -### D.6 Stacked Area Chart - -`chartType=areaStacked`, 3 series. Position: x=2cm, width=17cm, height=13cm. Include D.0 light-theme baseline. No `gap` step needed. - ---- - -## Section E: Tables - -> **CONSTRUCTION ORDER (violating this produces inconsistent fonts):** -> 1. Create table (`add --type table`) -> 2. Populate all rows with `set tr[N]` -> 3. Set table-level `size`/`font`/`border` -> 4. Apply cell-level styling (`fill`, `bold` only -- NOT cell-level `color`) - -> **Cell-level `color` produces validation errors.** Use row-level `color` when populating rows. For cell-level styling, use only `fill` and `bold`. - -### E.1 Comparison Table - -> **H-PALETTE reminder for comparison tables:** Do NOT use red to mark competitor weaknesses or green to mark your advantages. Use palette colors only: -> - Your product column header → `fill=$PRIMARY` or `fill=$SECONDARY`, `color=FFFFFF` -> - Competitor column headers → `fill=$DARK` or `fill=$DARK2`, `color=FFFFFF` (or light palette color on light background) -> - "Yes" / checkmark cells in your column → `fill=$LIGHT` + `bold=true` (NO green) -> - "No" / cross cells in competitor columns → leave default fill or use a subtle dark shade from palette (NO red) -> - Cell text for all status values (Yes/No/Partial/✓/✗) → same `color` as the row; do NOT change individual cell colors to red/green - -```bash -officecli add deck.pptx "/slide[N]" --type table \ - --prop rows=7 --prop cols=5 --prop x=2cm --prop y=4.5cm --prop width=29.87cm --prop height=13cm -officecli set deck.pptx "/slide[N]/table[1]/tr[1]" \ - --prop c1="Feature" --prop c2="DataFlow" --prop c3="Fivetran" --prop c4="Airbyte" --prop c5="Stitch" \ - --prop bold=true --prop fill=0F2B46 --prop color=FFFFFF --prop size=12 -officecli set deck.pptx "/slide[N]/table[1]/tr[2]" \ - --prop c1="No-code setup" --prop c2="Yes" --prop c3="Partial" --prop c4="No" --prop c5="Partial" --prop size=11 -# ... populate remaining rows, then table-level font ... -officecli set deck.pptx "/slide[N]/table[1]" --prop size=11 --prop font=Calibri --prop border=E0E0E0 -# Highlight "your product" column: cell-level fill + bold only (NO cell-level color, NO green) -officecli set deck.pptx "/slide[N]/table[1]/tr[2]/tc[2]" --prop fill=E8F0FE --prop bold=true -``` - -### E.2 Pricing Table (3-Tier) - -Same create -> populate -> table-level font -> cell-level fill pattern. 6 rows x 4 cols. Header: `fill=$PRIMARY, color=FFFFFF, bold, size=14`. Recommended column: highlight header cell `fill=F9AB00`, body cells `fill=F0EEFF` (fill only, no cell-level color). - -### E.3 Financial Impact Table (with Cell Merge) - -```bash -officecli add deck.pptx "/slide[N]" --type table \ - --prop rows=10 --prop cols=4 --prop x=2cm --prop y=5cm --prop width=29.87cm --prop height=13cm -# Header -officecli set deck.pptx "/slide[N]/table[1]/tr[1]" \ - --prop c1="" --prop c2="Without" --prop c3="With" --prop c4="Improvement" \ - --prop bold=true --prop fill=00D4AA --prop color=0D0D1A --prop size=12 -# Section header: populate THEN merge -officecli set deck.pptx "/slide[N]/table[1]/tr[2]" \ - --prop c1="INCIDENT METRICS" --prop bold=true --prop fill=1A1A3E --prop color=00D4AA --prop size=11 -officecli set deck.pptx "/slide[N]/table[1]/tr[2]/tc[1]" --prop merge.right=3 -# Data rows, more section headers, then table-level styling... -officecli set deck.pptx "/slide[N]/table[1]" --prop size=11 --prop font=Calibri --prop border=2A2A4A -``` - -`merge.right=3` merges tc[1] with 3 cells to its right (4-column span). After merge, only set content on tc[1]. See H-5 for validation warning. - ---- - -## Section F: Visual Polish - -### F.1 Gradient Backgrounds - -```bash -# Linear 2-color with angle -officecli add deck.pptx / --type slide --prop layout=blank --prop "background=0F2B46-0A1628-180" - -# Radial gradient -officecli add deck.pptx / --type slide --prop layout=blank --prop "background=radial:0D0D1A-1A1A3E-center" - -# Linear 3-color (no custom stops) -officecli set deck.pptx "/slide[N]" --prop "background=1E2761-4472C4-CADCFC" -``` - -> **Custom gradient stop positions (`@` syntax) are NOT supported for slide backgrounds.** Both `add` and `set` fail with "Invalid color value" when using `C1@0-C2@70-C3@100` on slide backgrounds. Use simple 2-color or 3-color gradients. The `@` syntax works only on shape `gradient` fills (e.g., `--prop "gradient=FF0000@0-FFFF00@50-00FF00@100"` on a shape). - -### F.2 Transitions - -```bash -# Title and closing slides -officecli set deck.pptx "/slide[1]" --prop transition=fade -officecli set deck.pptx "/slide[N]" --prop transition=fade - -# Content slides -officecli set deck.pptx "/slide[2]" --prop transition=push-left -``` - -Use at most 2-3 transition types per deck. Dark themes may use `fade` throughout. - -### F.3 Entrance Animations (Case Study Reveals Only) - -Use sparingly -- only for sequential stat reveals on case study slides. - -```bash -# First stat: click-triggered -officecli set deck.pptx "/slide[N]/shape[2]" --prop animation=fade-entrance-400-click - -# Subsequent stats: auto-after with 200ms delay -officecli set deck.pptx "/slide[N]/shape[3]" --prop animation=fade-entrance-400-after-delay=200 -officecli set deck.pptx "/slide[N]/shape[4]" --prop animation=fade-entrance-400-after-delay=200 -``` - -Transitions and animations coexist without conflict. - -### F.4 z-Order Management - -Add background shapes first, content shapes second to avoid z-order changes. If needed: `--prop zorder=back` / `zorder=front`. Warning: z-order changes renumber shape indices -- process highest index first. - -### F.5 Connectors - -Add all shapes BEFORE connectors. Title = shape[1], flow shapes start at shape[2]. Connectors can reference shapes from the same batch. - -```bash -officecli add deck.pptx "/slide[N]" --type connector \ - --prop preset=elbow --prop startShape=2 --prop endShape=3 \ - --prop line=1A73E8 --prop lineWidth=2pt --prop tailEnd=triangle -``` - -### F.6 Align and Distribute - -```bash -officecli set deck.pptx "/slide[N]" --prop distribute=horizontal --prop "targets=shape[2],shape[4],shape[6]" -officecli set deck.pptx "/slide[N]" --prop align=bottom --prop "targets=shape[2],shape[4],shape[6]" -``` - ---- - -## Section G: QA Checklist - -Run before every delivery. - -### Automated - -```bash -# Must return zero errors (see H-5 for cell merge exception) -officecli validate deck.pptx - -# Check for issues -officecli view deck.pptx issues -# NOTE: All slides using layout=blank will report "Slide has no title" -# This is EXPECTED behavior — pitch-deck uses blank layout (no built-in PowerPoint title -# placeholder). This warning is safe to ignore. Not a bug. - -# Visual preview per slide (use --start/--end, NOT --slide or --output which do not exist) -officecli view deck.pptx svg --start 1 --end 1 > /tmp/slide1.svg # slide 1 -officecli view deck.pptx svg --start 2 --end 2 > /tmp/slide2.svg # slide 2 -# For gradient-background slides: SVG renders gradient as white — use html mode for visual QA -officecli view deck.pptx html --browser -``` - -### Manual Verification - -- [ ] `officecli validate` = 0 errors (exception: cell merge slides may report schema warnings) -- [ ] Every chart is editable in PowerPoint (click -> Edit Data appears) -- [ ] Every table is a native table object (click -> table editing mode) -- [ ] No two consecutive slides share the same layout structure -- [ ] Speaker notes on every content slide (all except title and closing), >= 2 sentences each -- [ ] All text fully visible (no overflow, no overlap, no off-slide) -- [ ] At least one slide has 3+ stat callouts with number >= 36pt and label <= 16pt -- [ ] Gradient background on both slide 1 and last slide -- [ ] Color palette consistent across all slides (no random/default colors) -- [ ] **H-PALETTE:** No red/green/gray introduced for status indicators in comparison tables or flow diagrams — all colors trace back to declared palette variables -- [ ] **H-FONT font consistency:** Every shape has an explicit `font` value; only Georgia and Calibri appear across all shapes (no Arial, Helvetica, or default fonts) -- [ ] On dark-background slides, verify all text uses light colors (no default black text) -- [ ] Charts use modern styling (plotFill=none, chartFill=none, subtle gridlines) -- [ ] For any slide with connectors: take a screenshot/SVG preview and verify EACH connector is visible before proceeding. CLI may report success even when rendering fails (H-14). -- [ ] For any dual-series chart: verify both series are visible at scale. If one series appears invisible (ranges differ >10x), STOP and split into two charts immediately (D.4 HARD RULE). -- [ ] **Layout density check:** Content must occupy ≥ 60% of the canvas on every slide. If a slide has large empty zones (more than 40% whitespace), fix it by: (a) adding a supporting stat or data point, (b) adding a visual element (icon, chart, callout box), or (c) increasing the size of existing elements. Slides with only 2-3 small elements and large blank areas below will fail evaluation. -- [ ] **Connector overlap check:** For any slide with connectors or arrows, confirm in the PNG screenshot that no connector line or arrowhead overlaps a text box. If overlap is found, reposition or use `zorder=back` before delivery (see C.9). - ---- - -## Section H: Known Bugs & Lessons - -> **Read these before building. Each one has caused production failures.** -> -> **HARD RULE items are non-negotiable -- violation produces broken output. WARNING items are strong guidance that should be followed unless you have a specific reason not to. Known limitation items cannot be worked around.** - -| # | Issue | Workaround | -|---|-------|-----------| -| H-1 | `gap` ignored during chart `add` | Apply via `officecli set "/slide[N]/chart[1]" --prop gap=80` after creation | -| H-2 | Table font cascade overwritten by row `set` | Set table-level `size`/`font` AFTER all rows populated | -| H-3 | Shell `$` in batch JSON or `set` commands | **Batch:** use heredoc: `cat <<'EOF' \| officecli batch`. **Individual `set`:** single-quote the prop: `--prop 'c2=$499'`. Double-quoted `--prop "c2=$499"` silently expands `$499` to empty with no error. | -| H-4 | Combo chart requires both `comboSplit=1` AND `secondary=2` | Missing either causes incorrect rendering. Always include both | -| H-5 | Cell merge (`merge.right=N`) produces validation errors | PowerPoint renders correctly. Note in delivery message | -| H-6 | Cell-level `color` on table cells causes validation errors | Use row-level `color` instead; cell-level `fill` + `bold` only | -| H-7 | Custom gradient stops (`@`) fail on slide backgrounds | Use 2/3-color gradients. `@` syntax works only on shape `gradient` fills | -| H-8 | Connector shape indices: title = shape[1] | Flow shapes start at shape[2]. Count from first shape on slide | -| H-9 | z-order changes cause shape index renumbering | Process highest index first. Re-query with `get --depth 1` if needed | -| H-10 | Chart series count fixed at creation | Include ALL series in `add`. To add series, delete and recreate | -| H-11 | Dark theme text invisible (defaults to black) | Explicitly set light `color` on every text shape on dark backgrounds | -| H-12 | zsh glob-expands `[N]` in paths | Always double-quote: `"/slide[1]/chart[1]"` | -| H-13 | Batch threshold | Reliable for up to ~20 operations per batch. Split larger batches into groups of 15-20. Heredoc syntax mandatory | -| H-14 | Connector arrows may not all render in batch | Add connectors in separate batch after shapes. If still missing, add one at a time | -| H-15 | Doughnut chart `colors` parameter may not apply | CLI accepts the parameter without error but PowerPoint renders default colors. No workaround. Verify via screenshot. | -| H-16 | Empty table cell string `c1=""` causes validation error | Use a space character `c1=" "` instead of empty string for blank cells | diff --git a/skills/officecli-pptx/SKILL.md b/skills/officecli-pptx/SKILL.md index 55784e253..57e00d703 100644 --- a/skills/officecli-pptx/SKILL.md +++ b/skills/officecli-pptx/SKILL.md @@ -1,202 +1,114 @@ --- -# officecli: v1.0.23 name: officecli-pptx -description: "Use this skill any time a .pptx file is involved -- as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file; editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions 'deck,' 'slides,' 'presentation,' or references a .pptx filename." +description: "Use this skill any time a .pptx file is involved -- as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file; editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions 'deck', 'slides', 'presentation', 'pitch', or references a .pptx filename." --- # OfficeCLI PPTX Skill -## BEFORE YOU START (CRITICAL) +## Setup -> [!CAUTION] -> **zsh 用户(macOS 默认 shell)**:所有含方括号的路径参数**必须加引号**,否则 zsh 会 glob 展开并报错 `zsh: no matches found`。 -> - 正确:`officecli set deck.pptx '/slide[1]'` 或 `"/slide[1]"` -> - 错误:`officecli set deck.pptx /slide[1]`(zsh 会展开 `[1]`) -> -> **这是首次使用时几乎必然触发的错误。** 验证引号是否生效: -> ```bash -> officecli get deck.pptx '/slide[1]' --depth 1 # 正确(有引号) -> ``` -> 如果看到 `no matches found`,说明引号缺失。 +If `officecli` is missing: -**Every time before using officecli, run this check:** +- **macOS / Linux**: `curl -fsSL https://d.officecli.ai/install.sh | bash` +- **Windows (PowerShell)**: `irm https://d.officecli.ai/install.ps1 | iex` -```bash -if ! command -v officecli &> /dev/null; then - echo "Installing officecli..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - # Windows: irm https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.ps1 | iex -else - CURRENT=$(officecli --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - LATEST=$(curl -fsSL https://api.github.com/repos/iOfficeAI/OfficeCLI/releases/latest | grep '"tag_name"' | sed -E 's/.*"v?([0-9.]+)".*/\1/') - if [ "$CURRENT" != "$LATEST" ]; then - echo "Upgrading officecli $CURRENT → $LATEST..." - curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCli/main/install.sh | bash - else - echo "officecli $CURRENT is up to date" - fi -fi -officecli --version -``` - ---- - -## Quick Reference - -| Task | Action | -|------|--------| -| Read / analyze content | Use `view` and `get` commands below | -| Edit existing presentation | Read [editing.md](editing.md) | -| Create from scratch | Read [creating.md](creating.md) | - ---- - -## Execution Model - -**Run commands one at a time. Do not write all commands into a shell script and execute it as a single block.** - -OfficeCLI is incremental: every `add`, `set`, and `remove` immediately modifies the file and returns output. Use this to catch errors early: - -1. **One command at a time, then read the output.** Check the exit code before proceeding. -2. **Non-zero exit = stop and fix immediately.** Do not continue building on a broken state. -3. **Verify after structural operations.** After adding a slide, chart, table, or animation, run `get` or `validate` before building on top of it. - -Running a 50-command script all at once means the first error cascades silently through every subsequent command. Running incrementally means the failure context is immediate and local — fix it and move on. - ---- +Verify with `officecli --version` (open a new terminal if PATH hasn't picked up). If install fails, download a binary from https://github.com/iOfficeAI/OfficeCLI/releases. -## Reading & Analyzing +## ⚠️ Help-First Rule -### Text Extraction +**This skill teaches what good slides look like, not every command flag. When a property name, enum value, or alias is uncertain, consult help BEFORE guessing.** ```bash -officecli view slides.pptx text -officecli view slides.pptx text --start 1 --end 5 +officecli help pptx # List all pptx elements +officecli help pptx # Full element schema (e.g. shape, chart, animation, connector, zoom, group, background) +officecli help pptx # Verb-scoped (e.g. add shape, set slide) +officecli help pptx --json # Machine-readable schema ``` -### Structure Overview +Help reflects the installed CLI version. When skill and help disagree, **help is authoritative**. Triggers to run help immediately: `UNSUPPORTED props:` warning, unknown animation preset, `connector.shape=` enum drifts, prop-vs-alias (`lineWidth` vs `line.width`, `color` vs `font.color`). -```bash -officecli view slides.pptx outline -``` +## Shell & Execution Discipline -Output shows slide titles, shape counts, and picture counts per slide. +**Shell quoting (zsh / bash).** ALWAYS quote element paths (`"/slide[1]/..."`) — zsh globs unquoted `[1]` to `no matches found`. Escapes happen at two layers; the CLI handles one for you: -**注意:`view outline` 不计入表格和图表**——含表格/图表的 slide 显示为 "1 text box(es)",shape count 偏低。如需完整结构清单(含表格行列数和图表类型),请使用: -```bash -officecli view slides.pptx annotated -``` +1. **Shell.** `$` in a value still belongs to the shell — single-quote the whole value: `--prop text='$15M'`. Double-quoted `"$15M"` gets expanded to `M`. The CLI does NOT unescape `\$` for you. +2. **CLI (`text=`).** The two-char escapes `\n` and `\t` ARE interpreted, consistently across pptx / docx / xlsx — `\n` is a line / paragraph break, `\t` is a tab. To produce a literal backslash-n in text, double it (`\\n`); this is rarely what you want. +3. **JSON (batch).** Real newlines / tabs can also be passed as `"\n"` / `"\t"` inside a `<<'EOF'` heredoc; both forms produce the same result. -### Detailed Inspection +If in doubt, `view text` after writing and compare character-for-character. -```bash -officecli view slides.pptx annotated -``` +**Incremental execution.** One command → check exit code → continue. A 50-command script that fails at command 3 cascades silently. After any structural op (new slide, chart, animation, connector) run `get` before stacking more. -Shows shape types, fonts, sizes, pictures with alt text status, tables with dimensions. +## Requirements for Outputs -### Statistics +These are the deliverable standards every deck MUST meet. Violating any one = not done, regardless of content quality. -```bash -officecli view slides.pptx stats -``` +### All decks -Slide count, shape count, font usage, missing titles, missing alt text. +**One idea per slide.** If a slide needs a second title to explain what it covers, split it. Dense "everything about X" slides lose the audience inside 3 seconds. Use a section divider to group related one-idea slides, not a mega-slide. -### Element Inspection +**Explicit type hierarchy — do NOT rely on theme defaults.** Theme defaults drift between masters. Set sizes explicitly on every text shape. -```bash -# List all shapes on a slide -officecli get slides.pptx /slide[1] --depth 1 +| Element | Minimum | Typical | Min shape height | +|---|---|---|---| +| Slide title | **≥ 36pt** bold | 36–44pt | ≥ 2cm | +| Section / subtitle | ≥ 20pt | 20–24pt | ≥ 1.2cm | +| Body text | **≥ 18pt** | 18–22pt | ≥ 1cm | +| Caption / axis label | ≥ 10pt muted | 10–12pt | ≥ 0.6cm | -# Get shape details (position, fill, font, animation, etc.) -officecli get slides.pptx /slide[1]/shape[1] +Rule of thumb: **min shape height ≈ font_pt × 0.05cm**. An 18pt sublabel in a 0.8cm-tall box will overflow — `view annotated` catches this. -# Get chart data and config -officecli get slides.pptx /slide[1]/chart[1] +Title must be **≥ 2× body size** (36pt over 20pt works; 28pt over 20pt looks timid). Four legit exceptions to body ≥ 18pt: chart axis labels, legends, footer / page number, and ≤ 5-word KPI sublabels (e.g. "Active users"). Descriptive sentences must be ≥ 18pt. Left-align body; center only titles and hero numbers. If "the cards won't fit", drop cards instead of shrinking font. -# Get table structure -officecli get slides.pptx /slide[1]/table[1] --depth 3 +**Two fonts max, one palette.** One heading font + one body font (e.g. Georgia + Calibri). One dominant brand color (60–70% weight) + one supporting + one accent. Never mix 4+ colors in body content. -# Get placeholder by type -officecli get slides.pptx "/slide[1]/placeholder[title]" -``` +**Every slide carries a non-text visual.** Shape, chart, icon, gradient band. A bullet-only deck is interchangeable with a Word doc. Exceptions: literal quote slides, code blocks, a single summary-table slide. -### CSS-like Queries +**Speaker notes on every content slide.** `--type notes --prop text="..."`. The speaker needs a script; the audience shouldn't read the slide verbatim. -```bash -# Find shapes containing specific text -officecli query slides.pptx 'shape:contains("Revenue")' +**Preserve existing templates.** When a file already has a theme and masters, match them. Existing conventions override these guidelines. -# Find pictures without alt text -officecli query slides.pptx "picture:no-alt" +### Visual delivery floor (applies to EVERY deck) -# Find shapes with specific fill color -officecli query slides.pptx 'shape[fill=#4472C4]' +Before declaring done, the per-slide render (see QA) MUST satisfy: -# Find shapes wider than 10cm -officecli query slides.pptx "shape[width>=10cm]" +- **No placeholder tokens rendered as content.** `{{name}}`, `$fy$24`, ``, `lorem`, `xxxx`, empty `()`/`[]` in chart titles never appear. +- **No overflow past slide edges.** For 16:9 (33.87 × 19.05cm), every shape satisfies `x + width ≤ 33.87cm` AND `y + height ≤ 19.05cm`. `get` and check — don't eyeball. +- **No text overflow inside shapes.** A 72pt KPI in a 4cm-tall box clips. Shrink the number, enlarge the box, or shorten the text — never trim content to fit. +- **Cover slide is content-rich.** Title + subtitle + presenter/client block + date + a brand band or key-takeaway strap. A cover with 80% whitespace reads as a stub. +- **Contrast.** On fills with brightness < 30% (`1E2761`, `36454F`, `000000`, deep forest / berry / cherry), every run of body text, card body, chart series fill, and icon color must be `FFFFFF` or brightness > 80%. Mid-gray (`6B7B8D` ≈ 44%) reads fine on a laptop and vanishes on projection. Verify via `view html` after the dark-fill pass. +- **No `\$` literals in slide text.** If `view text` shows a literal `\$`, the shell didn't unescape it (the CLI does NOT interpret `\$`). Single-quote the value: `--prop text='$15M'`. Note: `\n` and `\t` ARE interpreted as a real paragraph break / tab; seeing those as literals means the value was double-escaped (`\\n`). -# Find shapes on a specific slide -officecli query slides.pptx 'slide[2] > shape[font="Arial"]' -``` +If any fails, STOP and fix before declaring done. -### Visual Inspection +### KPI fit math -```bash -# SVG rendering (single slide, self-contained, no dependencies) -officecli view slides.pptx svg --start 1 --end 1 --browser +**KPI text must fit the card — pre-compute, don't eyeball.** In a 7cm-wide card at 60pt Georgia bold, values with `$` and `.` (wide glyphs) wrap at 4 characters. `$9.4M` breaks the card; use `$9M` + "USD millions" sublabel, or move to the 3-card 9.78cm layout. Upper bound: `max_size_pt ≈ card_width_cm × denom`, where denom = 10 for 1–2 chars, 7 for 3–4 chars, 5 for 5+ chars. -# HTML rendering (all slides, interactive, with charts and 3D -- recommended) -officecli view slides.pptx html --browser -``` +### `layout=blank` and alt text -**Note:** SVG renders only one slide per invocation (the first in the range). Use `html --browser` for multi-slide preview with full chart/gradient/table rendering. - ---- +- **`layout=blank` is the default for custom designs.** Titles become plain `shape` elements, not placeholders. `view outline` / `view issues` reporting `(untitled)` / `Slide has no title` is **expected**, not a defect. Use `layout=title` + `placeholder[title]` only when screen-reader outline compatibility matters. +- **Alt text verification.** `view stats "Pictures without alt text: 0"` is a false-positive zero (alt auto-fills to filename) — verify via `view annotated`. ## Design Principles -**Don't create boring slides.** Plain bullets on a white background won't impress anyone. - -### Before Starting - -- **Pick a bold, content-informed color palette**: The palette should feel designed for THIS topic. If swapping your colors into a completely different presentation would still "work," you haven't made specific enough choices. -- **Dominance over equality**: One color should dominate (60-70% visual weight), with 1-2 supporting tones and one sharp accent. Never give all colors equal weight. -- **Dark/light contrast**: Dark backgrounds for title + conclusion slides, light for content ("sandwich" structure). Or commit to dark throughout for a premium feel. -- **Commit to a visual motif**: Pick ONE distinctive element and repeat it -- rounded image frames, icons in colored circles, thick single-side borders. Carry it across every slide. - -### Color Palettes - -Choose colors that match your topic -- don't default to generic blue: - -| Theme | Primary | Secondary | Accent | Text | Muted/Caption | -|-------|---------|-----------|--------|------|---------------| -| **Coral Energy** | `F96167` (coral) | `F9E795` (gold) | `2F3C7E` (navy) | `333333` (charcoal) | `8B7E6A` (warm gray) | -| **Midnight Executive** | `1E2761` (navy) | `CADCFC` (ice blue) | `FFFFFF` (white) | `333333` (charcoal) | `8899BB` (slate) | -| **Forest & Moss** | `2C5F2D` (forest) | `97BC62` (moss) | `F5F5F5` (cream) | `2D2D2D` (near-black) | `6B8E6B` (faded green) | -| **Charcoal Minimal** | `36454F` (charcoal) | `F2F2F2` (off-white) | `212121` (black) | `333333` (dark gray) | `7A8A94` (cool gray) | -| **Warm Terracotta** | `B85042` (terracotta) | `E7E8D1` (sand) | `A7BEAE` (sage) | `3D2B2B` (brown-black) | `8C7B75` (dusty brown) | -| **Berry & Cream** | `6D2E46` (berry) | `A26769` (dusty rose) | `ECE2D0` (cream) | `3D2233` (dark berry) | `8C6B7A` (mauve gray) | -| **Ocean Gradient** | `065A82` (deep blue) | `1C7293` (teal) | `21295C` (midnight) | `2B3A4E` (dark slate) | `6B8FAA` (steel blue) | -| **Teal Trust** | `028090` (teal) | `00A896` (seafoam) | `02C39A` (mint) | `2D3B3B` (dark teal) | `5E8C8C` (muted teal) | -| **Sage Calm** | `84B59F` (sage) | `69A297` (eucalyptus) | `50808E` (slate) | `2D3D35` (dark green) | `7A9488` (faded sage) | -| **Cherry Bold** | `990011` (cherry) | `FCF6F5` (off-white) | `2F3C7E` (navy) | `333333` (charcoal) | `8B6B6B` (dusty red) | +A deck is not a document. The audience has 3 seconds to get each slide. Before adding anything, ask: "If the audience reads only the biggest element and glances once, do they get the point?" If they have to read the bullets, the biggest element is wrong. -Use **Text** for body copy on light backgrounds, **Muted** for captions, labels, and axis text. On dark backgrounds, use the Secondary or `FFFFFF` for body text and Muted for captions. +### Grid, margins, negative space -> **深色背景对比度规则(Hard Rule H6 补充)**:当 slide 背景为深色(填充亮度 < 30%,如 `1E2761`、`36454F`、`000000` 等)时,所有正文文字、卡片 body text、图表系列颜色和图标填充**必须**使用白色(`FFFFFF`)或近白色(亮度 > 80%)。 -> **严禁**在深色背景上使用中性灰或低饱和色调(如 `6B7B8D`,亮度约 44%)作为 body text 颜色——这类颜色在深色背景上对比度不足,在演示现场尤为明显。 -> 验证方法:在完成深色背景 slide 后,用 `view html --browser` 或视觉 QA 子代理确认所有文字和元素清晰可辨。 +Standard widescreen is **33.87 × 19.05cm**. Treat it as a 12-column grid internally: -**Need a color not in the table?** These palettes are starting points. You can add accent colors (e.g., gold `D4A843` with Forest & Moss) or blend palettes to match the topic. If a user requests a palette that doesn't exist by name (e.g., "Forest & Gold"), use the closest match and supplement with appropriate accent tones. +- **Edge margin ≥ 1.27cm** (0.5") on all sides. +- **Inter-block gap ≥ 0.76cm** (0.3") between cards / columns / rows. +- **≥ 20% negative space per slide.** Filling every pixel reads as amateur. +- For card grids: `usable = 33.87 − 2·margin − (N−1)·gap`, then `col_width = usable / N`. Don't hand-pick x coordinates. -### Typography +### Font pairings -**Choose an interesting font pairing** -- don't default to Arial. +Two fonts max — one for headings, one for body. Pair by document register, not by novelty. "Best For" is a prompt, not a decree; if the topic matches a row, use it as the default and move on. -| Header Font | Body Font | Best For | -|-------------|-----------|----------| +| Header | Body | Best For | +|---|---|---| | Georgia | Calibri | Formal business, finance, executive reports | | Arial Black | Arial | Bold marketing, product launches | | Calibri | Calibri Light | Clean corporate, minimal design | @@ -204,488 +116,471 @@ Use **Text** for body copy on light backgrounds, **Muted** for captions, labels, | Trebuchet MS | Calibri | Friendly tech, startups, SaaS | | Impact | Arial | Bold headlines, event decks, keynotes | | Palatino | Garamond | Elegant editorial, luxury, nonprofit | -| Consolas | Calibri | Developer tools, technical/engineering | +| Consolas | Calibri | Developer tools, technical / engineering | -| Element | Size | -|---------|------| -| Slide title | 36-44pt bold | -| Section header | 20-24pt bold | -| Body text | **16-20pt**(最小 16pt;绝不低于 16pt) | -| Captions | 10-12pt muted | +Set both fonts explicitly on every shape (`--prop font=Georgia` on title shapes, `--prop font=Calibri` on body shapes) — theme-default inheritance drifts between masters. -> **Hard Rule H4**:body text 最低 **16pt**,无任何例外。 -> 卡片内正文、多列内容、bullet points 一律不低于 16pt。 -> 「内容放不下」不是低于 16pt 的理由——应减少文字、拆分 slide,或减少卡片数量。 -> 仅以下非主读元素允许 < 16pt:图表轴标签、图例、脚注、KPI 数字下方的说明标注(sublabel)。 -> -> **KPI sublabel 例外的适用范围**:仅限 ≤5 个词的短标注(如 "Active users"、"MoM growth"、"Q3 2025")。 -> 若 sublabel 是完整的描述性句子(如 "Compared to last quarter's baseline figure"),则不适用此例外,必须使用 ≥16pt 正文或删除该文字。 +### Color and contrast -> **Hard Rule H7**:所有内容 slide(非封面、非结尾 slide)**必须**包含演讲者备注(speaker notes)。 -> 使用 `officecli add deck.pptx /slide[N] --type notes --prop text="..."` 为每张内容 slide 添加备注。 -> 缺少 speaker notes 的内容 slide 是交付硬性失败项。 +One dominant color does 60–70% of visual weight, two supporting tones, one accent used sparingly. Never use 4+ colors in body content. Columns are: **Primary** (dominant — the one color you see first), **Secondary** (the supporting tone), **Accent** (sparing, one-hit emphasis), **Text** (body on light fills), **Muted** (captions / axis labels / footer). -### Layout Variety +| Theme | Primary | Secondary | Accent | Text | Muted | +|---|---|---|---|---|---| +| Coral Energy | `F96167` | `F9E795` | `2F3C7E` | `333333` | `8B7E6A` | +| Midnight Executive | `1E2761` | `CADCFC` | `FFFFFF` | `333333` | `8899BB` | +| Forest & Moss | `2C5F2D` | `97BC62` | `F5F5F5` | `2D2D2D` | `6B8E6B` | +| Charcoal Minimal | `36454F` | `F2F2F2` | `212121` | `333333` | `7A8A94` | +| Warm Terracotta | `B85042` | `E7E8D1` | `A7BEAE` | `3D2B2B` | `8C7B75` | +| Berry & Cream | `6D2E46` | `A26769` | `ECE2D0` | `3D2233` | `8C6B7A` | +| Ocean Gradient | `065A82` | `1C7293` | `21295C` | `2B3A4E` | `6B8FAA` | +| Teal Trust | `028090` | `00A896` | `02C39A` | `2D3B3B` | `5E8C8C` | +| Sage Calm | `84B59F` | `69A297` | `50808E` | `2D3D35` | `7A9488` | +| Cherry Bold | `990011` | `FCF6F5` | `2F3C7E` | `333333` | `8B6B6B` | -**Every slide needs a non-text visual element** — shape, color block, chart, icon, or graphic. Text-only slides are forgettable and violate delivery standards. +Pick by topic, not by default — finance reads Midnight Executive, a product launch reads Coral Energy, safety / LOTO reads Cherry Bold. If the closest named theme is not quite right, blend (e.g. Forest primary + gold `D4A843` accent). Use **Text** on light fills, **Muted** for captions / axis / footer, `FFFFFF` or Secondary for body on dark fills. -#### 无图片场景的视觉设计清单(CLI 限制下的替代方案) +On dark backgrounds, text and chart series follow the Hard rules contrast floor above. -officecli 不依赖外部图片文件即可实现丰富视觉效果。当无可用图片文件时,必须从以下至少一种方式中选取视觉元素: +### Chart-choice decision table -| 方式 | 实现方法 | 适用场景 | -|------|---------|---------| -| **色块背景** | `--type shape --prop fill=COLOR --prop preset=roundRect` | 卡片、强调区块 | -| **渐变 slide 背景** | `--prop "background=COLOR1-COLOR2-180"` | Section dividers、title slides | -| **Icon in circle** | 彩色 ellipse + 文字/数字居中叠加(见 creating.md)| 功能列表、流程步骤 | -| **大字号统计数字** | `--prop size=64 --prop bold=true`(60-72pt 数字)+ 小标签 | KPI、stats slides | -| **图表** | `--type chart`(column/pie/line 等) | 数据展示 slides | -| **形状组合** | circles + connectors + arrows 构建图表/流程 | 架构图、时间线 | +Wrong chart type kills the 3-second test: -**强制 checkpoint**:每 3 张 content slide 中,至少 1 张必须包含上述非文字视觉元素(色块/图形/图表)。纯文字 slide 仅允许在以下情况使用:引用(quote)、代码示例(code)、纯表格 slide。 +| Data shape | Use | Avoid | +|---|---|---| +| Category comparison (A vs B vs C) | `column` (vertical) / `bar` (≥ 6 categories, horizontal) | pie (slices merge), line (no time axis) | +| Time series, 1–3 series | `line` | area (occlusion), bar (implies discrete) | +| Part-of-whole, 2–5 slices | `pie` / `doughnut` | pie with 8+ slices (unreadable) | +| Correlation / distribution | `scatter` | line (implies ordering) | +| Multiple categories × metrics, dense | stacked `column` or heatmap | one chart per metric — consolidate | +| KPI snapshot (single big number) | **Large-text shape** (60–72pt + ≤ 5-word sublabel), NOT a chart | gauge chart, tiny bar | -Vary across these layout types: -- Two-column (text left, visual right) -- Icon + text rows (icon in colored circle, bold header, description) -- 2x2 or 2x3 grid (content blocks) -- Half-bleed image (full left/right side) with content overlay -- Large stat callouts (big numbers 60-72pt with small labels below) -- Comparison columns (before/after, pros/cons) -- Timeline or process flow (numbered steps, arrows) +Rule of thumb: if > 3 series and > 8 categories, split into two charts or switch to a table. -### Content-to-Layout Quick Guide +### Animation restraint -These are starting points. Adapt based on content density and narrative flow. +Each animation is a cognitive interrupt. Limits: +- **≤ 1 animation per slide**, duration **≤ 600ms**. +- Use only `fade`, `appear`, or a single `zoom-entrance` on a hero slide. +- Never: `bounce`, `swivel`, `fly-from-edge`, `spin`, multi-object choreography. +- Animation is runtime-only — verify in a live presentation viewer. -| Content Type | Recommended Layout | Why | -|---|---|---| -| Pricing / plan tiers | 2-3 column cards (comparison) | Side-by-side enables instant comparison | -| Team / people | Icon grid or 2x3 cards | Faces/avatars need equal visual weight | -| Timeline / roadmap | Process flow with arrows or numbered steps | Left-to-right communicates sequence | -| Key metrics / KPIs | Large stat callouts (3-4 big numbers) | Big numbers grab attention; labels below | -| Testimonials / quotes | Full-width quote with attribution | Generous whitespace signals credibility | -| Feature comparison | Two-column before/after or table | Parallel structure aids scanning | -| Architecture / system | Shapes + connectors diagram | Spatial relationships need visual expression | -| Financial data | Chart + summary table side-by-side | Chart shows trend; table provides precision | - -### Spacing - -- 0.5" (1.27cm) minimum margins from slide edges -- 0.3-0.5" (0.76-1.27cm) between content blocks -- Leave breathing room -- don't fill every inch - -### Avoid (Common Mistakes) - -- **Don't repeat the same layout** -- vary columns, cards, and callouts across slides -- **Don't center body text** -- left-align paragraphs and lists; center only titles -- **Don't skimp on size contrast** -- titles need 36pt+ to stand out from 14-16pt body -- **Don't default to blue** -- pick colors that reflect the specific topic -- **Don't mix spacing randomly** -- choose 0.3" or 0.5" gaps and use consistently -- **Don't style one slide and leave the rest plain** -- commit fully or keep it simple throughout -- **Don't create text-only slides** -- add images, icons, charts, or visual elements -- **Don't forget text box padding** -- when aligning shapes with text edges, set `margin=0` on the text box or offset to account for default padding -- **Don't use low-contrast elements** -- icons AND text need strong contrast against the background -- **NEVER use accent lines under titles** -- these are a hallmark of AI-generated slides; use whitespace or background color instead +### Layout patterns & data display ---- +Vary layout across slides — repeating the same pattern makes every slide feel identical. Pick one per slide from these building blocks: -## QA (Required) +| Pattern | When to use | Key measurement | +|---|---|---| +| **Two-column** (text left, visual right) | Concept + evidence; feature + screenshot | Each col ≈ 14-15cm; gap 1cm | +| **Icon rows** (icon in filled circle + bold header + description) | Feature lists, benefits, team roles | Icon circle 1.5-2cm; 3-4 rows max | +| **2×2 or 2×3 grid** (card tiles) | Quadrant analysis, SWOT, option comparison | Gap ≥ 0.76cm; consistent card height | +| **Half-bleed image** (full left or right half, content overlay on other side) | Hero moments, case study openers | Image 16-17cm wide; content column ≥ 14cm | +| **Large stat callout** (60-72pt number + ≤5-word sublabel below) | Single KPI, milestone, market size | Use shape, NOT a chart; sublabel 14-16pt muted | -**Assume there are problems. Your job is to find them.** +**Data display quick rules:** +- One big number reads faster than a chart — use a `shape` with 60-72pt bold for a single KPI. +- Comparison columns (before/after, A vs B) beat a table for 2-3 options. +- Timelines and process flows: numbered step shapes + connectors, not a bullet list. -Your first render is almost never correct. Approach QA as a bug hunt, not a confirmation step. If you found zero issues on first inspection, you weren't looking hard enough. +### Visual motif commitment -### Content QA +Pick ONE distinctive element (rounded image frames, section numbers in filled circles, single-side border band, diagonal accent strips) and carry it to every slide. Declare it in your build plan first: `## Motif: numbered circles in brand color`. -```bash -# Extract all text, check for missing content, typos, wrong order -officecli view slides.pptx text -``` +### What to avoid (common design mistakes) -> **注意:`view text` 不提取表格 (table) 内的文本。** 如需验证表格内容,请使用 -> `officecli get deck.pptx '/slide[N]/table[M]' --json` 检查各单元格内容。 -> 对于 QBR、技术规范等大量使用表格的幻灯片,仅靠 `view text` 会产生 QA 盲区。 +These are the patterns that make a deck look AI-generated or amateur: -```bash +- **NEVER place a decorative line under slide titles.** Underline stripes below headings are the single most common AI-slide tell. Use whitespace or background color change instead. +- **Don't repeat the same layout across consecutive slides.** Alternate between two-column, callout, grid, and half-bleed patterns. Same layout = same visual rhythm = audience tunes out. +- **Don't center body text.** Left-align all paragraphs, lists, card descriptions. Center only slide titles and hero numbers. +- **Don't default to blue** because it feels "professional." Pick the palette that fits the topic — finance reads navy, sustainability reads forest, energy reads coral. +- **Don't use inconsistent spacing.** Choose either 0.76cm or 1.27cm as your inter-block gap and use it everywhere. Mixed gaps look unfinished. +- **Don't create text-only slides.** If a slide has only a title and bullets, add a supporting shape, chart, icon, or image. A purely textual slide is a Word paragraph. +- **Don't style one slide and leave the rest plain.** Commit fully or keep it simple throughout — partial styling reads as abandoned. -# Check for structural and formatting issues automatically -officecli view slides.pptx issues -``` +## Common Workflow + +1. **Open/close mode.** Always `officecli open ` at start + `officecli close ` at end. Resident is the default, not an optimization. Use `batch` for repetitive shape grids. +2. **Orient.** New deck: `officecli create "$FILE"`. Existing: `officecli view "$FILE" outline` first. Never edit blind. +3. **Build in display order.** Add slides in audience-view order: cover → agenda → section-1 divider → section-1 content → section-2 divider → … → closing. `--index` on slide add works, but linear append keeps the build script readable and avoids index-arithmetic bugs. **Before final delivery, confirm slide count + narrative arc match your build plan.** Gate 3's order-sanity check catches cases where the cover ends up as slide 11 of 14 instead of slide 1. +4. **Incremental per slide.** Create slide + background, then title, then supporting shapes / charts / connectors. Always `layout=blank` for custom designs. After each structural op, `get /slide[N] --depth 1` to confirm shape IDs. +5. **Format to spec.** Per the Requirements table; formatting is deliverable, not polish. +6. **Close + verify.** `officecli close` writes the ZIP. Always open in the target presentation viewer before shipping — chart colors, animations, fonts, and zoom are runtime features `view html` can't render. Full verification in QA below. +7. **QA — assume there are problems.** Fix-and-verify until a cycle finds zero new issues. -**Note:** `view issues` reports "Slide has no title" for all blank-layout slides. This is expected when using `layout=blank` (the recommended approach for custom designs). These warnings can be safely ignored. +## Quick Start -When editing templates, check for leftover placeholder text: +Minimal viable deck: cover + one content slide + notes. `$FILE` stands in for your filename. ```bash -officecli query slides.pptx 'shape:contains("lorem")' -officecli query slides.pptx 'shape:contains("xxxx")' -officecli query slides.pptx 'shape:contains("placeholder")' +FILE="deck.pptx" +officecli create "$FILE" +officecli open "$FILE" + +# Cover — dark fill, centered title +officecli add "$FILE" / --type slide --prop layout=blank --prop background=1E2761 +officecli add "$FILE" /slide[1] --type shape --prop text="FY26 Strategic Review" \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=3cm \ + --prop font=Georgia --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center + +# Content — white fill, title + body + notes +officecli add "$FILE" / --type slide --prop layout=blank --prop background=FFFFFF +officecli add "$FILE" /slide[2] --type shape --prop text="Revenue grew 18% YoY" \ + --prop x=1.5cm --prop y=1.2cm --prop width=30cm --prop height=2cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 +officecli add "$FILE" /slide[2] --type shape --prop text="Enterprise renewals + new EMEA region drove the beat; NRR held at 118%." \ + --prop x=1.5cm --prop y=4cm --prop width=30cm --prop height=3cm \ + --prop font=Calibri --prop size=20 --prop color=333333 +officecli add "$FILE" /slide[2] --type notes --prop text="Lead with the 18% beat, preview EMEA." + +officecli close "$FILE" +officecli validate "$FILE" ``` -### Visual QA +Shape of every build: open → slide+background → title → body → notes → close → validate. -**Use subagents** -- even for 2-3 slides. You've been staring at the code and will see what you expect, not what's there. Subagents have fresh eyes. +## Reading & Analysis -```bash -# Render a single slide as SVG for visual inspection -officecli view slides.pptx svg --start 3 --end 3 --browser +Start wide, then narrow. `outline` first, `view text` / `get` / `query` once you know where to look. -# Loop through slides for multi-slide QA -for i in 1 2 3 4 5; do officecli view slides.pptx svg --start $i --end $i > /tmp/slide-$i.svg; done +```bash +officecli view "$FILE" outline # slide count + titles +officecli view "$FILE" annotated # complete per-slide breakdown with fonts, sizes, tables, charts +officecli view "$FILE" text --start 1 --end 5 # text dump (does NOT extract table cells — use get) +officecli view "$FILE" issues # empty slides, overflow hints +officecli view "$FILE" stats # counts + missing alt (false-positive zero — verify via view annotated) ``` -**SVG limitations:** SVG renders only one slide (the first in the `--start`/`--end` range). Gradient backgrounds, charts, and tables are not visible in SVG output. For full-fidelity multi-slide preview including charts and gradients, use HTML mode: +**Inspect one element.** XPath-style paths, 1-based. ALWAYS quote. Prefer `@name=` / `@id=` selectors over positional `[N]` (stable across reorderings). `[last()]` works. Add `--json` for machine output. ```bash -officecli view slides.pptx html --browser +officecli get "$FILE" "/slide[1]" --depth 1 # shape list with IDs and names +officecli get "$FILE" "/slide[1]/shape[@name=Title]" +officecli get "$FILE" "/slide[1]/table[1]" --depth 3 # table rows / cells ``` -Prompt for visual QA subagent: +**Query across the deck.** CSS-like selectors; operators `=`, `!=`, `~=`, `>=`, `<=`, `[attr]`, `:contains()`, `:no-alt`. `help pptx query` lists queryable element types. -``` -Visually inspect these slides. Assume there are issues -- find them. - -Look for: -- Overlapping elements (text through shapes, lines through words, stacked elements) -- Text overflow or cut off at edges/box boundaries -- Elements too close (< 0.3" gaps) or cards/sections nearly touching -- Uneven gaps (large empty area in one place, cramped in another) -- Insufficient margin from slide edges (< 0.5") -- Columns or similar elements not aligned consistently -- Low-contrast text (e.g., light gray on cream background) -- Low-contrast icons (e.g., dark icons on dark backgrounds without a contrasting circle) -- Text boxes too narrow causing excessive wrapping -- Leftover placeholder content - -For each slide, list issues or areas of concern, even if minor. -Report ALL issues found. +```bash +officecli query "$FILE" 'shape:contains("Revenue")' +officecli query "$FILE" 'picture:no-alt' # accessibility gap +officecli query "$FILE" 'shape[fill=1E2761]' # color match +officecli query "$FILE" 'shape[width>=10cm]' # numeric ``` -**Editing-specific QA checklist (in addition to the above):** -- [ ] On every template slide (not new blank slides), verify that NO decorative element (`!!`-prefixed shape) overlaps or obscures content text -- [ ] Verify all hero numbers / key metrics are visible (not hidden by card fills or same-color-as-background) -- [ ] On dark background slides, verify chart bars/lines, axis labels, and gridlines are visible +**`query --json` output schema.** Results wrap in `.data.results[]` — `jq -r '.data.results[0].format.id'`, NOT `.[0].id`. Shape name is `.name`; fill is `.format.fill`; textColor is `.format.textColor`. -### Validation +**Visual preview (LEAD).** ```bash -# Schema validation -- must pass before delivery -officecli validate slides.pptx +officecli view "$FILE" html # prints an HTML preview path; Read it for per-slide visual audit (best structural ground truth) +officecli view "$FILE" svg --start 3 --end 3 # single slide SVG (charts + gradients do NOT render in SVG) ``` -### Pre-Delivery Checklist +## Creating & Editing -Before declaring a presentation complete, verify: +Verbs: `add` / `set` / `remove` / `move` / `swap` / `batch` / `raw-set`. Ninety percent of a deck is slides, shapes, text, a few charts, pictures, connectors. -- [ ] **(Hard Rule H7)Speaker notes 验证**:使用 `officecli view deck.pptx annotated` 确认每张内容 slide(非封面、非结尾)均有 speaker notes 条目。缺少 notes 的内容 slide 是交付硬性失败项。 -- [ ] At least one transition style applied (fade for title, push or wipe for content) -- [ ] Alt text on all pictures -- [ ] At least 3 different layout types used across slides -- [ ] No two consecutive slides share the same layout pattern -- [ ] `view issues` "Slide has no title" warnings — **expected and safe to ignore** when using `layout=blank`. All custom designs use blank layout; these warnings are not real issues. -- [ ] **溢出检查(每张 slide 必做)**:对每张 slide 上的所有文字框和形状,确认 `y + height ≤ 19.05cm`(标准 widescreen 高度)且 `x + width ≤ 33.87cm`(标准宽度)。如有溢出,调小字号或缩短文本,**不得依赖截断**。 -- [ ] **卡片布局逐格溢出检查**:对多卡片布局(step cards、feature grids、timeline flows),逐张卡片验证 `y + height ≤ 19.05cm`。使用 `officecli get deck.pptx '/slide[N]/shape[M]'` 逐一检查每张卡片——不得基于卡片数量估算,必须逐格测量。 -- [ ] **Agenda 一致性**:如有 Agenda/TOC slide,确认其列出的所有 section 与实际 slide 标题和顺序完全一致,不得遗漏任何 section。 -- [ ] **字号合规(Hard Rule H4)**:所有 body text、卡片正文、bullet points、多列内容的字号 ≥ 16pt。允许 < 16pt 的例外仅限:图表轴标签、图例、KPI sublabel(≤5 词的短标注)、脚注。 +### Slides and backgrounds -> **Hard Rule H4 澄清**:body text ≥ 16pt 无例外。若内容放不下, -> 解决方案是减少文字或拆分 slide,而非缩小字号。 -> 允许 < 16pt 的例外:图表轴标签、图例、KPI sublabel(**仅限 ≤5 词的短标注**,如 "Active users"、"MoM growth";完整描述性句子不适用此例外)、脚注。 +A slide is `/slide[N]`. Always pass `layout=blank` for custom designs. Background: solid, gradient, or image. -- [ ] **图表标题无空占位符**:所有图表标题不得含有 `()`、`[]`、`TBD`、`XXX` 等空占位符。 - 若标题包含动态内容(如单位 `$M`),必须在 QA 阶段替换为实际值。 - 检查命令:`officecli view slides.pptx text` 然后搜索 `"()"`。 +```bash +officecli add "$FILE" / --type slide --prop layout=blank --prop background=1E2761 # solid +officecli add "$FILE" / --type slide --prop layout=blank --prop "background=1E2761-CADCFC-180" # gradient (start-end-angle) +officecli add "$FILE" / --type slide --prop layout=blank --prop "background.image=hero.jpg" # image background (LEAD) +``` -### Verification Loop +### Shapes -1. Generate slides -2. Run `view issues` + `validate` + visual inspection -3. **List issues found** (if none found, look again more critically) -4. Fix issues -5. **Re-verify affected slides** -- one fix often creates another problem -6. Repeat until a full pass reveals no new issues +A `shape` holds text, fill, border, position, and optional animation / link. -**Do not declare success until you've completed at least one fix-and-verify cycle.** +```bash +officecli add "$FILE" /slide[2] --type shape --prop name=Title --prop text="Key Insight" \ + --prop x=2cm --prop y=2cm --prop width=20cm --prop height=3cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 --prop fill=none +``` ---- +Positioning is explicit — no layout engine, you own the grid math. `--prop preset=` picks geometry (`rect`, `roundRect`, `ellipse`, `triangle`, `arrow`, `star5`, ...); custom `M...Z` paths are not supported — pick a preset. **Name shapes at creation** (`--prop name=HeroTitle`) and address later with `"/slide[N]/shape[@name=HeroTitle]"` — positional `/shape[3]` breaks after any z-order / remove. -## Common Pitfalls +> **Prefer `@name=` over `@id=`.** Names you set yourself survive remove-then-add and z-order ops cleanly. After any structural change, re-`get --depth 1` before referencing positional indexes. -| Pitfall | Correct Approach | -|---------|-----------------| -| ⚠️ Unquoted `[N]` in zsh/bash | Shell glob-expands `/slide[1]` and throws `no matches found`. **Always quote paths**: `"/slide[1]"` or `'/slide[1]'`. This is the #1 first-use stumbling block on zsh. | -| `--name "foo"` | Use `--prop name="foo"` -- all attributes go through `--prop` | -| `x=-3cm` | Negative coordinates **are supported** and can be used for bleed effects (e.g., `x=-2cm` lets a decorative element overflow the left edge). | -| `/shape[myname]` | Name indexing not supported. Use numeric index: `/shape[3]` | -| Guessing property names | Run `officecli pptx set shape` to see exact names | -| `\n`/`\\` in shell strings & code slides | 普通文本 shape:使用 `\\n` 表示换行,如 `--prop text="line1\\nline2"`。
**代码 slide 特别注意**:`--prop text="kubectl apply \\n -f pod.yaml"` 会在 slide 上显示字面量 `\\n`(而非换行)。对于演示用代码内容,使用单个 `\n` 实现真实换行:`--prop text="line1\nline2"`。但在 shell 单引号字符串中 `\n` 是字面量;建议使用 heredoc 或 JSON batch 传递带换行的代码文本,以避免 shell 转义问题。 | -| Modifying an open file | Close the file in PowerPoint/WPS first | -| Hex colors with `#` | Use `FF0000` not `#FF0000` -- no hash prefix | -| Theme colors | Use `accent1`..`accent6`, `dk1`, `dk2`, `lt1`, `lt2` -- not hex | -| Forgetting alt text | Always set `--prop alt="description"` on pictures for accessibility | -| Paths are 1-based | `/slide[1]`, `/shape[1]` -- XPath convention | -| `--index` is 0-based | `--index 0` = first position -- array convention | -| Z-order (shapes overlapping) | Use `--prop zorder=back` or `zorder=front` / `forward` / `backward` / absolute position number. **WARNING:** Z-order changes cause shape index renumbering -- re-query with `get --depth 1` after any z-order change before referencing shapes by index. Process highest index first when changing multiple shapes. | -| `gap`/`gapwidth` on chart add | Ignored during `add` -- set it after creation: `officecli set ... /slide[N]/chart[M] --prop gap=80` | -| `$` in `--prop text=` (shell) | `--prop text="$15M"` strips the value — shell expands `$15` as a variable. Use single quotes: `--prop text='$15M'`. For multiline or mixed quotes, use heredoc batch. | -| `$` and `'` in batch JSON text | Use heredoc: `cat <<'EOF' \| officecli batch` -- single-quoted delimiter prevents shell expansion of `$`, apostrophes, and backticks | -| Template text at wrong size | Template shapes have baked-in font sizes. Always include `size`, `font`, and `color` in every `set` on template shapes. See editing.md "Font Cascade from Template Shapes" section. | +### Text inside shapes (paragraphs, runs, styling) ---- +A shape has paragraphs (`paragraph[K]`) and runs. For one-line text, `--prop text=` on the shape is enough. Multi-line or mixed styling: -## Recipes(常见场景修复指南) +```bash +# add --type paragraph accepts only text + align; styling goes through a follow-up set or an add --type run: +officecli add "$FILE" "/slide[2]/shape[@name=Card1]" --type paragraph --prop text="First bullet" +officecli set "$FILE" "/slide[2]/shape[@name=Card1]/paragraph[1]" --prop bold=true --prop size=20 --prop color=FFFFFF -以下配方针对实际制作中高频出现的视觉问题,每条均为可直接执行的修复方案。 +# Styled run in one step: +officecli add "$FILE" "/slide[2]/shape[@name=Card1]/paragraph[1]" --type run \ + --prop text=" (inline detail)" --prop size=14 --prop italic=true --prop color=8899BB +``` -### Recipe 1:Section Divider — 标签文字与装饰元素重叠 +For real newlines inside one run, use a batch heredoc with JSON `"\n"`. Shell-quoted `\n` in `--prop text=` is NOT interpreted. -**问题根因:** 后添加的 shape 在 z-order 上层;若装饰 shape(圆、矩形)在文字 shape 之后添加,会覆盖文字,导致标题不可读。 +### Charts -**修复规则:** -1. **添加顺序即 z-order**:装饰元素(圆、色块)必须先添加,文字 shape 后添加——后添加的自动在最上层。 -2. **标题文字 y 位置建议 7–10cm**(slide 高 19.05cm),避免与顶部或底部装饰元素重叠。 -3. 若需调整已有 shape 的层级,使用 `--prop zorder=back`(装饰元素)或 `--prop zorder=front`(文字)。 +Pick chart type per the Design Principles chart-choice table. Full prop list (chartType enum, `seriesN.*`, `data=`/`categories=`, axis options): `help pptx add chart`. Typical multi-series with brand colors: ```bash -# 正确顺序示例(装饰先,文字后) -officecli add slides.pptx / --type slide --prop layout=blank --prop "background=1E2761-CADCFC-180" - -# 第1步:装饰元素(大半透明数字作为背景图形)— 先添加,在底层 -officecli add slides.pptx /slide[N] --type shape --prop text="02" \ - --prop x=2cm --prop y=4cm --prop width=29.87cm --prop height=8cm \ - --prop font=Georgia --prop size=120 --prop bold=true \ - --prop color=FFFFFF --prop align=center --prop fill=none --prop opacity=0.15 - -# 第2步:左侧装饰色条(可选)— 装饰元素,在底层 -officecli add slides.pptx /slide[N] --type shape \ - --prop preset=rect --prop fill=FFFFFF --prop opacity=0.2 \ - --prop x=0cm --prop y=7cm --prop width=6cm --prop height=0.4cm --prop line=none - -# 第3步:标题文字 — 最后添加,自动在最上层,y 建议 7–10cm -officecli add slides.pptx /slide[N] --type shape --prop text="Financial Performance" \ - --prop x=2cm --prop y=7.5cm --prop width=29.87cm --prop height=3cm \ - --prop font=Georgia --prop size=40 --prop bold=true \ - --prop color=FFFFFF --prop align=center --prop fill=none - -# 第4步:副标题(可选) -officecli add slides.pptx /slide[N] --type shape --prop text="Section 2 of 4" \ - --prop x=2cm --prop y=11cm --prop width=29.87cm --prop height=1.5cm \ - --prop font=Calibri --prop size=16 --prop color=CADCFC --prop align=center --prop fill=none +officecli add "$FILE" /slide[3] --type chart --prop chartType=column \ + --prop series1.name=Revenue --prop series1.values="42,45,48" --prop series1.color=1E2761 \ + --prop series2.name=Growth --prop series2.values="2,7,7" --prop series2.color=CADCFC \ + --prop categories="Q1,Q2,Q3" \ + --prop x=2cm --prop y=4cm --prop width=20cm --prop height=10cm ``` -**事后检查(如遇覆盖问题):** -```bash -# 将装饰元素压到最底层 -officecli set slides.pptx "/slide[N]/shape[1]" --prop zorder=back -# 将文字拉到最顶层 -officecli set slides.pptx "/slide[N]/shape[3]" --prop zorder=front -# 注意:zorder 操作后 shape index 会重新编号,须重新 get --depth 1 再操作 -officecli get slides.pptx '/slide[N]' --depth 1 -``` +Gotchas: (1) series cannot be added after creation — include all series at `add` time or `remove` + re-add. (2) chart titles with `()`, `[]`, `TBD` ship as literal text. (3) some viewers normalize chart colors to theme defaults — verify in the target viewer. ---- +### Pictures -### Recipe 2:KPI Box — 数字/文字溢出 box 边界 +```bash +officecli add "$FILE" /slide[4] --type picture --prop src=hero.jpg \ + --prop x=1cm --prop y=1cm --prop width=32cm --prop height=18cm \ + --prop alt="Product hero, gradient lit from right" +``` -**问题根因:** KPI 数字字号过大,超出 box 的 height 或 width 范围;或 box 尺寸未为数字字号留足空间。 +Confirm with `officecli query "$FILE" 'picture:no-alt'` — must be empty before delivery (but remember `view stats` is a false-positive zero because alt auto-fills to filename). -**字号安全公式:** -- `推荐最大字号(pt) ≤ box_width_cm × 字符数分母` - - 1–2 个字符(如 "94%"):`box_width_cm × 10` pt 为上限,建议用 60–72pt - - 3–4 个字符(如 "1.2M"):`box_width_cm × 7` pt 为上限,建议用 48–56pt - - 5+ 个字符:`box_width_cm × 5` pt 为上限,建议用 36–44pt -- `box height ≥ 字号(cm) × 1.5`(字号 1pt ≈ 0.0353cm;64pt ≈ 2.26cm,则 height ≥ 3.4cm) +### Connectors (LEAD — flowcharts / decision trees first-class) -**验证规则(必做):** 每个 KPI box 创建后,用 `officecli view annotated` 确认无溢出。 +Draws a line between two shapes or free coordinates. Full prop / enum reference (`shape`, `headEnd`/`tailEnd` values, `from`/`to` ref forms): `help pptx add connector`. ```bash -# KPI box 安全模板(以 9cm 宽 box、3字符数字为例) -# 9cm 宽 × 3 字符 → 最大字号约 9×7=63pt → 使用 60pt -# box height ≥ 60pt × 0.0353cm × 1.5 ≈ 3.2cm → 设为 4cm(留余量) - -officecli add slides.pptx /slide[N] --type shape \ - --prop text="94%" \ - --prop x=2cm --prop y=5cm \ - --prop width=9cm --prop height=4cm \ - --prop font=Georgia --prop size=60 --prop bold=true \ - --prop color=CADCFC --prop align=center --prop valign=center --prop fill=none - -# sublabel(KPI 说明标注,≤5 词,允许 < 16pt) -officecli add slides.pptx /slide[N] --type shape \ - --prop text="Customer Retention" \ - --prop x=2cm --prop y=9.2cm \ - --prop width=9cm --prop height=1.5cm \ - --prop font=Calibri --prop size=13 --prop color=8899BB --prop align=center --prop fill=none +officecli add "$FILE" /slide[5] --type connector \ + --prop "from=/slide[5]/shape[@name=BoxA]" --prop "to=/slide[5]/shape[@name=BoxB]" \ + --prop shape=elbow --prop color=333333 --prop tailEnd=triangle ``` -**溢出修复流程:** -1. 发现溢出 → 先缩小字号(每次减 4pt,重新检查) -2. 字号已足够小但仍溢出 → 扩大 box `height`(y 值相应上移) -3. 不得缩短数字本身("$1.2M" 不能改成 "$1M" 只为字号合规) +**Every flow connector needs an arrowhead.** Without one, `bentConnector3` renders as a directionless line. `preset=rightArrow` overlay only works for horizontal flows; diamonds / decision trees with diverging edges need `tailEnd=`. + +### Animations (LEAD) + +One preset per slide, ≤ 600ms. Preset names + duration syntax: `help pptx animation`. ```bash -# 验证命令 -officecli view slides.pptx annotated -# 检查每个 KPI shape 的 y+height 是否 ≤ 19.05cm -officecli get slides.pptx '/slide[N]/shape[M]' +officecli set "$FILE" "/slide[2]/shape[@name=HeroCard]" --prop animation=fade-entrance-400 +officecli set "$FILE" "/slide[2]/shape[@name=HeroCard]" --prop animation=none # clear all ``` ---- +### Hyperlinks, tooltips, slide-jump -### Recipe 3:Timeline — 最后节点孤立(间距不均匀) +`--prop link=slide:N` for slide-jump, `link=https://...` for URL, `--prop tooltip="..."` for hover text. (Help only documents the URL form — `slide:N` is skill-only knowledge.) -**问题根因:** 直接将最后节点 x 设为 `slide_width - right_margin` 时,浮点精度差异导致其与相邻节点间距偏大,视觉上"孤立"。 +### Tables, placeholders, groups, zoom — one-liners -**均匀间距公式:** -``` -left_margin = 2cm(或按设计) -right_margin = 2cm(或按设计) -circle_width = 节点圆的宽度(例如 3cm) +- **Tables** — `--type table --prop rows=N --prop cols=M`. Row-level `set` supports `height`, `header`, `c1/c2/c3`. Cell formatting lives on the cell paragraph / run. Populate rows BEFORE setting table-level font (font cascade gets reset by row ops). +- **Placeholders** — `"/slide[N]/placeholder[title]"` / `placeholder[body]`. Available only when the slide uses a layout with placeholders (not `layout=blank`). +- **Groups** (LEAD) — address children via `"/slide[N]/group[@name=G]/shape[1]"`. Survives reordering better than positional indexes. +- **Zoom slide** (LEAD) — `--type zoom --prop targets="3,7,15"`. Section-navigation hub. Zoom is a runtime feature — `view html` shows the static geometry; the zoom interaction runs only in a live presentation viewer. +- **Slide comments** — reviewer annotations anchored at `/slide[N]/comment[M]`. Full lifecycle (`add / set / get / query / remove`). Props: `text`, `author`, `initials` (auto-derived), `date` (ISO 8601, defaults to UtcNow), `x` / `y` (length anchor). + ```bash + officecli add "$FILE" "/slide[2]" --type comment --prop author="Alice" --prop text="Tighten this bullet" --prop x=20cm --prop y=3cm + officecli query "$FILE" 'comment' --json | jq '.data.results | length' # count all review comments + officecli remove "$FILE" "/slide[2]/comment[1]" # resolve after addressing + ``` -# CRITICAL: usable_width 必须减去 circle_width,否则最后节点右边界会溢出幻灯片 -usable_width = slide_width - left_margin - right_margin - circle_width - = 33.87 - 2 - 2 - 3 = 26.87cm(标准 16:9,circle_width=3cm) +### Deck-level recipes -node_spacing = usable_width / (N - 1) # N = 节点总数 +Patterns not obvious from the primitives. Each gives the **visual outcome** first, then a runnable block. `$FILE` = your filename. Use `/slide[last()]` to address the slide you just added. -node_x[i] = left_margin + node_spacing × i # i = 0, 1, ..., N-1 -``` +**Z-order.** Later-added shapes are on top. Add background decoration FIRST, titles LAST. To fix after the fact: `--prop zorder=back/front` (renumbers siblings — re-`get --depth 1` before stacking more). -> **为什么减 circle_width?** `node_x[i]` 是圆的**左边 x**,最后节点右边界 = `node_x[N-1] + circle_width`。不减的话右边界会超出幻灯片边缘(33.87cm),导致 P1 截断错误。 +#### (a) Cover (and section divider) -**示例(4 节点,节圆宽 3cm):** -``` -usable_width = 33.87 - 2 - 2 - 3 = 26.87cm -node_spacing = 26.87 / 3 ≈ 8.957cm - -node_x[0] = 2cm → circle x=2cm, 右边 5cm ✓ -node_x[1] = 2 + 8.957 = 10.957cm → circle x=10.96cm, 右边 13.96cm ✓ -node_x[2] = 2 + 8.957×2 = 19.914cm → circle x=19.91cm, 右边 22.91cm ✓ -node_x[3] = 2 + 8.957×3 = 28.87cm → circle x=28.87cm, 右边 31.87cm ✓ (< 33.87) -``` +**Visual outcome.** Dark navy fill, centered 44pt title, 18pt ice-blue meta line. ```bash -# 4 节点均匀时间轴示例(node_spacing ≈ 8.957cm,圆宽 3cm,usable_width=26.87cm) -# 水平基准线(从第一节点圆心到最后节点圆心) -officecli add slides.pptx /slide[N] --type connector \ - --prop x=3.5cm --prop y=10cm --prop width=27.87cm --prop height=0 \ - --prop line=CADCFC --prop lineWidth=2pt - -# 节点 1(i=0) x = 2cm,右边 5cm ✓ -officecli add slides.pptx /slide[N] --type shape \ - --prop preset=ellipse --prop fill=1E2761 \ - --prop x=2cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop line=none -officecli add slides.pptx /slide[N] --type shape --prop text="Q1" \ - --prop x=2cm --prop y=8.5cm --prop width=3cm --prop height=3cm \ - --prop fill=none --prop color=FFFFFF --prop size=16 --prop bold=true \ - --prop align=center --prop valign=center - -# 节点 2(i=1) x = 2 + 8.957 = 10.957cm → 取 10.96cm,右边 13.96cm ✓ -officecli add slides.pptx /slide[N] --type shape \ - --prop preset=ellipse --prop fill=CADCFC \ - --prop x=10.96cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop line=none -officecli add slides.pptx /slide[N] --type shape --prop text="Q2" \ - --prop x=10.96cm --prop y=8.5cm --prop width=3cm --prop height=3cm \ - --prop fill=none --prop color=1E2761 --prop size=16 --prop bold=true \ - --prop align=center --prop valign=center - -# 节点 3(i=2) x = 2 + 8.957×2 = 19.914cm → 取 19.91cm,右边 22.91cm ✓ -officecli add slides.pptx /slide[N] --type shape \ - --prop preset=ellipse --prop fill=1E2761 \ - --prop x=19.91cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop line=none -officecli add slides.pptx /slide[N] --type shape --prop text="Q3" \ - --prop x=19.91cm --prop y=8.5cm --prop width=3cm --prop height=3cm \ - --prop fill=none --prop color=FFFFFF --prop size=16 --prop bold=true \ - --prop align=center --prop valign=center - -# 节点 4(i=3) x = 2 + 8.957×3 = 28.871cm → 取 28.87cm,右边 31.87cm ✓ (< 33.87) -officecli add slides.pptx /slide[N] --type shape \ - --prop preset=ellipse --prop fill=CADCFC \ - --prop x=28.87cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop line=none -officecli add slides.pptx /slide[N] --type shape --prop text="Q4" \ - --prop x=28.87cm --prop y=8.5cm --prop width=3cm --prop height=3cm \ - --prop fill=none --prop color=1E2761 --prop size=16 --prop bold=true \ - --prop align=center --prop valign=center +officecli add "$FILE" / --type slide --prop layout=blank --prop background=1E2761 +officecli add "$FILE" "/slide[last()]" --type shape --prop text="Strategic Growth Review" \ + --prop x=2cm --prop y=7cm --prop width=29.87cm --prop height=3cm \ + --prop font=Georgia --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center +officecli add "$FILE" "/slide[last()]" --type shape --prop text="Prepared for Acme Leadership — FY26 Outlook" \ + --prop x=2cm --prop y=11cm --prop width=29.87cm --prop height=1.2cm \ + --prop font=Calibri --prop size=18 --prop color=CADCFC --prop align=center ``` -**验证命令:** 创建时间轴后,检查各节点 x 坐标是否均匀分布: +**Section divider** = same cover, plus a giant translucent number (`size=120`, `opacity=0.15`) added FIRST so it sits behind the section title. + +#### (b) Data slide (chart + commentary block) + +**Visual outcome.** Left two-thirds: column chart with brand series colors. Right one-third: "Key Insight" card with 20pt heading + 18pt body — audience reads the takeaway before parsing the bars. + ```bash -officecli view slides.pptx annotated -# 或逐节点检查 -officecli get slides.pptx '/slide[N]' --depth 1 -# 手动验证相邻节点的 x 差值是否一致(允许 ±0.05cm 误差) +officecli add "$FILE" / --type slide --prop layout=blank --prop background=FFFFFF +officecli add "$FILE" "/slide[last()]" --type shape --prop text="FY26 Revenue Beat Plan by 18%" \ + --prop x=1.5cm --prop y=1cm --prop width=30cm --prop height=1.8cm \ + --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 + +# Chart — left 2/3 (single-quote the title because of `$`) +officecli add "$FILE" "/slide[last()]" --type chart --prop chartType=column \ + --prop series1.name=Actual --prop series1.values="42,45,48,55" --prop series1.color=1E2761 \ + --prop series2.name=Plan --prop series2.values="40,42,45,48" --prop series2.color=CADCFC \ + --prop categories="Q1,Q2,Q3,Q4" --prop x=1.5cm --prop y=3.5cm --prop width=20cm --prop height=14cm --prop title='FY26 Revenue ($M)' + +# Commentary card — right 1/3: background + heading + body +officecli add "$FILE" "/slide[last()]" --type shape --prop preset=roundRect --prop fill=F5F7FA --prop line=none \ + --prop x=22.5cm --prop y=3.5cm --prop width=9.8cm --prop height=14cm +officecli add "$FILE" "/slide[last()]" --type shape --prop text="Key Insight" \ + --prop x=23cm --prop y=4cm --prop width=9cm --prop height=1.2cm \ + --prop font=Georgia --prop size=20 --prop bold=true --prop color=1E2761 +officecli add "$FILE" "/slide[last()]" --type shape --prop text="EMEA launch + NRR at 118% drove 12pp of the 18pp beat." \ + --prop x=23cm --prop y=5.5cm --prop width=9cm --prop height=11cm \ + --prop font=Calibri --prop size=18 --prop color=333333 ``` -如发现最后节点孤立:计算实际间距(`x[N-1] - x[N-2]` vs `x[1] - x[0]`),用均匀间距公式重新设置最后节点的 x 坐标: +#### (c) Flowchart / process diagram (boxes + connectors) + +**Visual outcome.** Four rounded boxes across at y=8cm, each 6×3cm, alternating navy/iceblue, joined by elbow connectors with triangle arrowheads. + +Grid math (4 boxes, 33.87cm slide, 1.5cm margins): `gap = (33.87 − 3 − 24) / 3 = 2.29cm`. x-positions: `1.5, 9.79, 18.08, 26.37`. + +Each box carries its own label via `valign=middle` (no separate overlay shape needed). Use `batch` heredoc for portable coordinate arithmetic — no `bc`, no bash arrays. + ```bash -officecli set slides.pptx "/slide[N]/shape[M]" --prop x=31.87cm +cat <&1 | grep -vE "Slide has no title") +echo "$ISSUES" | grep -qE "^\s*\[[A-Z][0-9]+\]" && { echo "REJECT Gate 2:"; echo "$ISSUES"; exit 1; } || echo "Gate 2 OK" + +# Gate 2b — leftover placeholders ("xxxx", "lorem", "", empty (), [], "this slide layout") +LEFT=$(officecli view "$FILE" text | grep -niE 'xxxx|lorem|ipsum||placeholder|this[- ]slide[- ]layout|\(\)|\[\]') +[ -n "$LEFT" ] && { echo "REJECT Gate 2b:"; echo "$LEFT"; exit 1; } || echo "Gate 2b OK" +``` + +### Gate 3 — Visual audit (MANDATORY) -## Help System +Pick **one** path: -**When unsure about property names, value formats, or command syntax, run help instead of guessing.** One help query is faster than guess-fail-retry loops. +**Screenshot (default)** — needs image-Read + a headless browser. **Loop per slide** (viewport screenshot covers only slide 1): ```bash -officecli pptx set # All settable elements and their properties -officecli pptx set shape # Shape properties in detail -officecli pptx set shape.fill # Specific property format and examples -officecli pptx add # All addable element types -officecli pptx view # All view modes -officecli pptx get # All navigable paths -officecli pptx query # Query selector syntax +n=1 +while officecli view "$FILE" screenshot --page $n -o "/tmp/gate3_$n.png" 2>/dev/null; do + n=$((n+1)) +done +[ $n -eq 1 ] && { echo "no headless backend — using fallback"; SCREENSHOT_FAILED=1; } ``` + +Read each PNG against the checklist; delegate to a subagent when the harness has one. + +**Fallback — HTML-text** (no image-Read or no browser): read `view "$FILE" html` as text. DOM cannot prove **dark-on-dark / fine overlap / arrowheads / gap-margin metrics / column alignment** — flag these as "not visually verified" rather than PASS. + +**Optional `--grid N`** — only on user request for layout-rhythm, or when `view outline` shows anomalous layout distribution: `officecli view "$FILE" screenshot --grid 3 -o /tmp/grid.png`. + +**Per-slide checklist (assume issues exist):** + +- **overlap** — shapes / charts / giant decorative numbers (01/02/03 100pt+) colliding +- **text overflow** — clipped at slide or shape boundary (KPI cards, narrow boxes) +- **narrow text box** — content fits technically but wraps to many short lines (1–2 words each); long sublabel in a 3cm KPI card, body line in a too-tight column +- **dark-on-dark** — fill brightness < 30% with text/icon brightness < 80% (incl. dark icons on dark without a contrasting circle) +- **missing arrowheads** — flowchart connectors as plain lines +- **decorative-line / title mismatch** — accent bar sized for one-line title but title wrapped to two (or vice versa) +- **footer / citation collision** — source line, page number, or footnote touching content above +- **tight margin / gap** — element within ~0.5" of slide edge, or two cards within ~0.3" +- **uneven gaps** — large empty area on one side, cramped on another (broken rhythm) +- **column / repeat-element misalignment** — KPI cards / icons off baseline or inconsistent width +- **order sanity** — sequence matches narrative (cover → agenda → dividers-before-sections → closing) + +REJECT with `slide N: ` lines, else "Gate 3 PASS" (HTML-text fallback adds " not visually verified"). + +**Fix-verify (mandatory, max 3 cycles).** Fix → re-run Gate 3 → repeat until zero new issues; one fix often surfaces another. After 3 rounds without convergence, **stop** — likely seesaw, template-level cause, or agent misread. Report `slide N: — attempted: — likely root: ` and let the user decide. + +## Common Pitfalls + +Sanity-check cheatsheet — what breaks on the first try. Design + shell traps. + +| Pitfall | Correct approach | +|---|---| +| Unquoted `[N]` in zsh/bash | Always quote paths: `"/slide[1]"`. zsh globs unquoted `[1]` → `no matches found` — #1 first-use stumble | +| `--name "foo"` | All attributes go through `--prop`: `--prop name="foo"` | +| `/shape[myname]` (bare name in brackets) | Use `@name=` selector: `/shape[@name=myname]` or `/shape[@id=10007]` | +| Paths 1-based vs `--index` 0-based | `/slide[1]` = first slide; `--index 0` = first position | +| `$` in `--prop text=` | Single-quote: `--prop text='$15M'`. Double-quoted `"$15M"` gets shell-expanded to `M` | +| `\n` / `\t` in `--prop text=` | CLI does NOT interpret. Use multiple `--type paragraph`, or batch heredoc with JSON `"\n"` | diff --git a/skills/officecli-pptx/creating.md b/skills/officecli-pptx/creating.md deleted file mode 100644 index ddbd06949..000000000 --- a/skills/officecli-pptx/creating.md +++ /dev/null @@ -1,980 +0,0 @@ - - -# Creating Presentations from Scratch - -> [!CAUTION] -> **zsh 用户(macOS 默认 shell)**:所有含方括号的路径参数**必须加引号**,否则 zsh 会报错 `zsh: no matches found`。 -> - 正确:`officecli set deck.pptx '/slide[1]'` 或 `"slide[1]"` -> - 错误:`officecli set deck.pptx /slide[1]` -> -> **这是首次创建演示文稿时最常见的错误,请在第一条命令前就确认引号已加好。** -> -> 快速验证命令: -> ```bash -> officecli get deck.pptx '/slide[1]' --depth 1 # 有引号 = 正确 -> officecli get deck.pptx /slide[1] --depth 1 # 无引号 = zsh 报错 -> ``` - -Use this guide when creating a new presentation with no template. - -## Workflow Overview - -1. **Create** blank presentation -2. **Plan** slide structure (content outline + layout types) -3. **Build** each slide — one command at a time; use `batch` only for bulk repetitive operations (many shapes with identical props, filling many text boxes). Structural operations (`add slide`, `add chart`, `set transition`) should run individually so errors surface immediately. -4. **QA** (content + visual + validation) -- see [SKILL.md](SKILL.md#qa-required) - ---- - -## A. Visual Elements Strategy(必读) - -**officecli 不需要外部图片文件**即可制作视觉丰富的演示文稿。当无图片文件可用时,使用以下策略实现"非文字视觉元素"要求。 - -> **强制要求**:每 3 张 content slide 中,至少 1 张必须包含以下非文字视觉元素之一。纯文字 slide 只允许用于引用(quote)、代码示例、纯表格。 - -### 视觉元素类型及实现方式 - -**1. Icon in Colored Circle(最通用)** -```bash -# 彩色圆形背景 -officecli add slides.pptx /slide[N] --type shape --prop preset=ellipse --prop fill=1E2761 --prop x=2cm --prop y=5cm --prop width=2.5cm --prop height=2.5cm --prop line=none -# 中心文字(数字、emoji、字母均可) -officecli add slides.pptx /slide[N] --type shape --prop text="01" --prop x=2cm --prop y=5cm --prop width=2.5cm --prop height=2.5cm --prop fill=none --prop color=FFFFFF --prop size=16 --prop bold=true --prop align=center --prop valign=center -``` - -**2. 色块强调区(filled rectangle/roundRect)** -```bash -# 作为背景色块或分隔条 -officecli add slides.pptx /slide[N] --type shape --prop preset=roundRect --prop x=2cm --prop y=4cm --prop width=14cm --prop height=12cm --prop fill=E8EDF3 --prop line=CADCFC --prop lineWidth=1pt -``` - -**3. 大字号统计数字(Stats callout)** -```bash -# 60-72pt 数字 + 小标签 = 数字本身就是视觉元素 -officecli add slides.pptx /slide[N] --type shape --prop text="94%" --prop x=2cm --prop y=5cm --prop width=9cm --prop height=4cm --prop font=Georgia --prop size=64 --prop bold=true --prop color=CADCFC --prop align=center --prop fill=none -``` - -**4. 图表** -```bash -# 任意图表类型均可作为视觉元素 -officecli add slides.pptx /slide[N] --type chart --prop chartType=column --prop categories="Q1,Q2,Q3,Q4" --prop series1="Revenue:42,58,65,78" --prop x=2cm --prop y=4cm --prop width=20cm --prop height=12cm --prop colors=1E2761,CADCFC -``` - -**5. 渐变背景(Section/Title slides)** -```bash -officecli set slides.pptx /slide[N] --prop "background=1E2761-CADCFC-180" -``` - -**Visual checkpoint**:在完成每 3 张 slide 后,运行 `officecli view slides.pptx outline` 检查 picture count + shape count,确保非文字元素存在。 - ---- - -## Setup - -```bash -# Create blank presentation (16:9 default) -officecli create slides.pptx - -# Open in resident mode — keep in memory for all slide operations -officecli open slides.pptx - -# Set metadata -officecli set slides.pptx / --prop title="Q4 Report" --prop author="Team Alpha" - -# Change slide size if needed -officecli set slides.pptx / --prop slideSize=16:9 -# Other presets: 4:3, 16:10, a4, a3, letter, widescreen -# Custom: --prop slideWidth=24cm --prop slideHeight=13.5cm - -# ... all slide operations ... - -# Close and validate when done -officecli close slides.pptx -officecli validate slides.pptx -``` - -Default 16:9 dimensions: 33.867cm x 19.05cm (13.333in x 7.5in). - ---- - -## Slide Structure Patterns - -Each pattern below is a complete recipe. Adapt colors, fonts, and positions to your palette. - -### Title Slide (Dark Background) - -```bash -# Add slide with dark background -officecli add slides.pptx / --type slide --prop layout=blank --prop background=1E2761 - -# Large centered title -officecli add slides.pptx /slide[1] --type shape --prop text="Quarterly Business Review" --prop x=2cm --prop y=5cm --prop width=29.87cm --prop height=4cm --prop font=Georgia --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center --prop valign=center --prop fill=none - -# Subtitle -officecli add slides.pptx /slide[1] --type shape --prop text="Q4 2025 | Finance Division" --prop x=2cm --prop y=10cm --prop width=29.87cm --prop height=2cm --prop font=Calibri --prop size=20 --prop color=CADCFC --prop align=center --prop fill=none - -# Date/footer -officecli add slides.pptx /slide[1] --type shape --prop text="December 2025" --prop x=2cm --prop y=16cm --prop width=29.87cm --prop height=1.5cm --prop font=Calibri --prop size=12 --prop color=8899BB --prop align=center --prop fill=none -``` - -### Content Slide (Title + Body Text) - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=F5F5F5 - -# Title bar with accent background -# margin format: left,top,right,bottom (text inset padding) -officecli add slides.pptx /slide[2] --type shape --prop text="Key Findings" --prop x=0cm --prop y=0cm --prop width=33.87cm --prop height=3.5cm --prop fill=1E2761 --prop color=FFFFFF --prop font=Georgia --prop size=32 --prop bold=true --prop align=left --prop valign=center --prop margin=2cm,0cm,0cm,0cm - -# Body text (left-aligned, bulleted) -officecli add slides.pptx /slide[2] --type shape --prop "text=Revenue grew 25% year-over-year\\nCustomer retention rate reached 94%\\nNew market expansion on track\\nOperating costs reduced by 12%" --prop x=2cm --prop y=5cm --prop width=29.87cm --prop height=12cm --prop font=Calibri --prop size=18 --prop color=333333 --prop list=bullet --prop align=left --prop fill=none --prop lineSpacing=1.5x -``` - -### Two-Column Layout (Text + Visual) - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=FFFFFF - -# Title -officecli add slides.pptx /slide[3] --type shape --prop text="Market Overview" --prop x=2cm --prop y=1cm --prop width=29.87cm --prop height=3cm --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 --prop align=left --prop fill=none - -# Left column -- text -officecli add slides.pptx /slide[3] --type shape --prop "text=Our market position has strengthened significantly in Q4.\\n\\nThree key drivers:\\n 1. Product innovation\\n 2. Strategic partnerships\\n 3. Customer-first approach" --prop x=2cm --prop y=5cm --prop width=14cm --prop height=12cm --prop font=Calibri --prop size=16 --prop color=333333 --prop align=left --prop fill=none --prop lineSpacing=1.4x - -# Right column -- image or chart placeholder -officecli add slides.pptx /slide[3] --type shape --prop preset=roundRect --prop x=18cm --prop y=5cm --prop width=14cm --prop height=12cm --prop fill=E8EDF3 --prop line=CADCFC --prop lineWidth=1pt - -# Or add an actual image: -# officecli add slides.pptx /slide[3] --type picture --prop path=market-chart.png --prop x=18cm --prop y=5cm --prop width=14cm --prop height=12cm --prop alt="Market share chart" -``` - -### Stats / Callout Slide (Big Numbers) - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=1E2761 - -# Title -officecli add slides.pptx /slide[4] --type shape --prop text="By The Numbers" --prop x=2cm --prop y=1cm --prop width=29.87cm --prop height=2.5cm --prop font=Georgia --prop size=32 --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=none - -# Stat 1 -officecli add slides.pptx /slide[4] --type shape --prop text="25%" --prop x=2cm --prop y=5cm --prop width=9cm --prop height=4cm --prop font=Georgia --prop size=64 --prop bold=true --prop color=CADCFC --prop align=center --prop valign=bottom --prop fill=none -officecli add slides.pptx /slide[4] --type shape --prop text="Revenue Growth" --prop x=2cm --prop y=9.5cm --prop width=9cm --prop height=2cm --prop font=Calibri --prop size=14 --prop color=8899BB --prop align=center --prop fill=none - -# Stat 2 -officecli add slides.pptx /slide[4] --type shape --prop text="94%" --prop x=12.5cm --prop y=5cm --prop width=9cm --prop height=4cm --prop font=Georgia --prop size=64 --prop bold=true --prop color=CADCFC --prop align=center --prop valign=bottom --prop fill=none -officecli add slides.pptx /slide[4] --type shape --prop text="Customer Retention" --prop x=12.5cm --prop y=9.5cm --prop width=9cm --prop height=2cm --prop font=Calibri --prop size=14 --prop color=8899BB --prop align=center --prop fill=none - -# Stat 3 -officecli add slides.pptx /slide[4] --type shape --prop text="1.2M" --prop x=23cm --prop y=5cm --prop width=9cm --prop height=4cm --prop font=Georgia --prop size=64 --prop bold=true --prop color=CADCFC --prop align=center --prop valign=bottom --prop fill=none -officecli add slides.pptx /slide[4] --type shape --prop text="Active Users" --prop x=23cm --prop y=9.5cm --prop width=9cm --prop height=2cm --prop font=Calibri --prop size=14 --prop color=8899BB --prop align=center --prop fill=none -``` - -### Chart Slide (Modern Styled) - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=FFFFFF - -# Title -officecli add slides.pptx /slide[5] --type shape --prop text="Revenue Trend" --prop x=2cm --prop y=1cm --prop width=29.87cm --prop height=2.5cm --prop font=Georgia --prop size=32 --prop bold=true --prop color=1E2761 --prop align=left --prop fill=none - -# Modern styled chart -officecli add slides.pptx /slide[5] --type chart --prop chartType=column --prop title="Quarterly Revenue ($M)" --prop categories="Q1,Q2,Q3,Q4" --prop series1="2024:42,58,65,78" --prop series2="2025:51,67,74,92" --prop x=2cm --prop y=4cm --prop width=29.87cm --prop height=14cm --prop colors=1E2761,CADCFC --prop legend=bottom --prop plotFill=none --prop chartFill=none --prop gridlines="E2E8F0:0.5" --prop dataLabels=value --prop labelPos=outsideEnd --prop labelFont="10:64748B:false" --prop axisFont="10:64748B:Calibri" --prop legendFont="10:64748B:Calibri" --prop title.font=Georgia --prop title.size=14 --prop title.color=333333 --prop series.outline="FFFFFF-0.5" -``` - -### Comparison Slide (Before / After) - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=F5F5F5 - -# Title -officecli add slides.pptx /slide[6] --type shape --prop text="Before & After" --prop x=2cm --prop y=1cm --prop width=29.87cm --prop height=2.5cm --prop font=Georgia --prop size=32 --prop bold=true --prop color=1E2761 --prop align=center --prop fill=none - -# Left card -- Before -officecli add slides.pptx /slide[6] --type shape --prop preset=roundRect --prop x=2cm --prop y=4.5cm --prop width=14.5cm --prop height=13cm --prop fill=FFFFFF --prop line=E0E0E0 --prop lineWidth=1pt -officecli add slides.pptx /slide[6] --type shape --prop text="Before" --prop x=3cm --prop y=5cm --prop width=12.5cm --prop height=2cm --prop font=Georgia --prop size=24 --prop bold=true --prop color=B85042 --prop align=left --prop fill=none -officecli add slides.pptx /slide[6] --type shape --prop "text=Manual data entry\\nFragmented workflows\\n48-hour turnaround\\nError rate: 12%" --prop x=3cm --prop y=7.5cm --prop width=12.5cm --prop height=9cm --prop font=Calibri --prop size=16 --prop color=555555 --prop list=bullet --prop align=left --prop fill=none --prop lineSpacing=1.6x - -# Right card -- After -officecli add slides.pptx /slide[6] --type shape --prop preset=roundRect --prop x=17.5cm --prop y=4.5cm --prop width=14.5cm --prop height=13cm --prop fill=FFFFFF --prop line=E0E0E0 --prop lineWidth=1pt -officecli add slides.pptx /slide[6] --type shape --prop text="After" --prop x=18.5cm --prop y=5cm --prop width=12.5cm --prop height=2cm --prop font=Georgia --prop size=24 --prop bold=true --prop color=2C5F2D --prop align=left --prop fill=none -officecli add slides.pptx /slide[6] --type shape --prop "text=Automated pipelines\\nUnified platform\\n4-hour turnaround\\nError rate: 0.3%" --prop x=18.5cm --prop y=7.5cm --prop width=12.5cm --prop height=9cm --prop font=Calibri --prop size=16 --prop color=555555 --prop list=bullet --prop align=left --prop fill=none --prop lineSpacing=1.6x -``` - -**Z-order tip:** When adding card backgrounds (roundRect) and then text on top, always add the background shape first. If shapes overlap incorrectly, fix with `--prop zorder=back` on the background shape or `--prop zorder=front` on the text shape. - -> **多卡片布局逐格溢出验证(必做)**:在完成任何卡片式布局(step cards、feature grids、before/after 对比卡、timeline flows)后,**逐张卡片**检查溢出,而非整体估算。 -> -> ```bash -> # 逐一检查每张卡片的形状坐标 -> officecli get slides.pptx '/slide[N]/shape[M]' -> ``` -> -> 对每张卡片,确认:`y + height ≤ 19.05cm`(垂直不溢出)且 `x + width ≤ 33.87cm`(水平不溢出)。 -> -> **不得基于卡片数量或布局规律估算**——必须逐格测量。卡片内容通常比预期更高(文字折行、padding 叠加),估算往往低估实际高度。如发现溢出:缩小字号、减少卡片内文字行数、或上移所有卡片的起始 y 坐标。 - -### Section Divider - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop "background=1E2761-CADCFC-180" - -# Section number -officecli add slides.pptx /slide[7] --type shape --prop text="02" --prop x=2cm --prop y=5cm --prop width=29.87cm --prop height=3cm --prop font=Georgia --prop size=72 --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=none --prop opacity=0.3 - -# Section title -officecli add slides.pptx /slide[7] --type shape --prop text="Financial Performance" --prop x=2cm --prop y=8cm --prop width=29.87cm --prop height=3cm --prop font=Georgia --prop size=40 --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=none -``` - -### Closing / CTA Slide - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=1E2761 - -# Main message -officecli add slides.pptx /slide[8] --type shape --prop text="Thank You" --prop x=2cm --prop y=5cm --prop width=29.87cm --prop height=4cm --prop font=Georgia --prop size=48 --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=none - -# Contact / CTA -officecli add slides.pptx /slide[8] --type shape --prop "text=Questions? Reach out at team@company.com" --prop x=2cm --prop y=10cm --prop width=29.87cm --prop height=2cm --prop font=Calibri --prop size=18 --prop color=CADCFC --prop align=center --prop fill=none -``` - ---- - -## Building Blocks - -### Shapes & Text Boxes - -```bash -# Simple text box -officecli add slides.pptx /slide[1] --type shape --prop text="Hello" --prop x=2cm --prop y=3cm --prop width=10cm --prop height=3cm - -# Styled shape with fill -officecli add slides.pptx /slide[1] --type shape --prop text="Important" --prop x=5cm --prop y=5cm --prop width=15cm --prop height=3cm --prop fill=4472C4 --prop color=FFFFFF --prop size=24 --prop bold=true --prop align=center --prop preset=roundRect - -# Shape without text (decorative) -officecli add slides.pptx /slide[1] --type shape --prop preset=ellipse --prop fill=FF6600 --prop x=15cm --prop y=5cm --prop width=5cm --prop height=5cm - -# Gradient fill -officecli add slides.pptx /slide[1] --type shape --prop text="Gradient Box" --prop x=2cm --prop y=2cm --prop width=10cm --prop height=5cm --prop gradient=4472C4-1A1A2E --prop color=FFFFFF - -# Z-order: send a card background behind text shapes -officecli set slides.pptx "/slide[1]/shape[5]" --prop zorder=back -# Or bring a shape to front -officecli set slides.pptx "/slide[1]/shape[3]" --prop zorder=front -# Relative: move one step back/forward -officecli set slides.pptx "/slide[1]/shape[3]" --prop zorder=backward - -# WARNING: Z-order changes cause shape index renumbering! -# After any zorder change, shape indices shift immediately. -# Re-query with `get --depth 1` before referencing shapes by index. -# When changing z-order for multiple shapes, process highest index first. - -# Radial gradient -officecli add slides.pptx /slide[1] --type shape --prop x=2cm --prop y=2cm --prop width=10cm --prop height=5cm --prop "gradient=radial:FFFFFF-4472C4-center" - -# IMPORTANT: gradient fills use the `gradient` property, NOT `fill`. -# fill=COLOR1-COLOR2 will ERROR -- use gradient=COLOR1-COLOR2 instead. - -# Custom gradient stops -officecli add slides.pptx /slide[1] --type shape --prop x=2cm --prop y=2cm --prop width=10cm --prop height=5cm --prop "gradient=FF0000@0-FFFF00@50-00FF00@100" - -# Bulleted list -officecli add slides.pptx /slide[1] --type shape --prop "text=First point\\nSecond point\\nThird point" --prop list=bullet --prop x=2cm --prop y=5cm --prop width=20cm --prop height=8cm --prop size=16 - -# Numbered list -officecli add slides.pptx /slide[1] --type shape --prop "text=Step one\\nStep two\\nStep three" --prop list=numbered --prop x=2cm --prop y=5cm --prop width=20cm --prop height=8cm -``` - -**Shape presets:** rect, roundRect, ellipse, diamond, triangle, rtTriangle, parallelogram, trapezoid, pentagon, hexagon, octagon, star4, star5, star6, star8, star10, star12, star16, star24, star32, heart, cloud, lightning, sun, moon, rightArrow, leftArrow, upArrow, downArrow, chevron, plus, cross, ribbon - -**List styles:** bullet, dash, arrow, check, star, numbered, alpha, roman, none - -**Text styling props:** `font`, `size`, `bold`, `italic`, `underline`, `color`, `align`, `valign`, `lineSpacing` (e.g. `1.5x`), `letterSpacing` / `charSpacing` (points, e.g. `2` for 2pt expanded), `margin` (text inset: `left,top,right,bottom`) - -**Shape opacity:** Use `--prop opacity=N` where N is 0.0–1.0 (0.0 = fully transparent, 1.0 = fully opaque). Useful for semi-transparent overlays, watermark-style labels, and decorative elements that should recede visually (e.g., `--prop opacity=0.3` for a ghost number behind content). - -#### Icon in Colored Circle Pattern - -There is no dedicated `icon` element type. Build icons from a colored circle + centered text (emoji, number, or letter): - -```bash -# Colored circle background -officecli add slides.pptx /slide[1] --type shape --prop preset=ellipse --prop fill=1E2761 --prop x=2cm --prop y=5cm --prop width=2.5cm --prop height=2.5cm --prop line=none - -# Centered text overlay (emoji, number, or letter as icon) -officecli add slides.pptx /slide[1] --type shape --prop text="01" --prop x=2cm --prop y=5cm --prop width=2.5cm --prop height=2.5cm --prop fill=none --prop color=FFFFFF --prop size=16 --prop bold=true --prop align=center --prop valign=center --prop font=Calibri - -# Or use an SVG icon file -officecli add slides.pptx /slide[1] --type picture --prop path=icon.svg --prop x=2.3cm --prop y=5.3cm --prop width=1.9cm --prop height=1.9cm --prop alt="Settings icon" -``` - -For icon + text rows, repeat the pattern at consistent vertical intervals (e.g., y=5cm, y=8.5cm, y=12cm) with a bold label and description text box to the right of each circle. - -**Overflow check(每张 slide 必做):** 标准 widescreen 尺寸为 **33.87cm × 19.05cm**。 - -- 垂直:`y + height ≤ 19.05cm`(超出则内容被裁切) -- 水平:`x + width ≤ 33.87cm`(超出则内容溢出右边) - -示例检查:如果某行高 2cm 且从 y=17.5cm 开始,则末端在 19.5cm —— 超出 slide 底边 0.45cm,**必须修正**。处理方式:调小字号、减少行数、或将第一行上移。**不得依赖 PowerPoint 的自动截断**——被截断的内容在演示时完全不可见。 - -在完成每张 slide 的所有 shape 后,运行以下命令验证: -```bash -officecli get slides.pptx '/slide[N]' --depth 1 -``` -检查每个 shape 的 `y`/`height`/`x`/`width` 数值,确认均在范围内。 - -### Aligning & Distributing Shapes - -After placing multiple shapes, align and distribute them for precise layouts: - -```bash -# Align specific shapes to slide center -officecli set slides.pptx "/slide[1]" --prop align=slide-center --prop "targets=shape[1],shape[2]" - -# Align shapes to each other (left edges) -officecli set slides.pptx "/slide[1]" --prop align=left --prop "targets=shape[1],shape[2],shape[3]" - -# Distribute shapes evenly (horizontal spacing) -officecli set slides.pptx "/slide[1]" --prop distribute=horizontal --prop "targets=shape[1],shape[2],shape[3]" - -# Distribute vertically -officecli set slides.pptx "/slide[1]" --prop distribute=vertical --prop "targets=shape[2],shape[3],shape[4]" -``` - -Align values (shape-relative): left, center, right, top, middle, bottom -Align values (slide-relative): slide-left, slide-center, slide-right, slide-top, slide-middle, slide-bottom - -Omit `targets` to apply to all shapes on the slide. - -### Multi-Paragraph Text (Rich Text) - -**When to use rich text vs. \\n:** -- Use `\\n` within a single `--prop text="..."` for simple same-style paragraphs -- Use paragraph/run operations when you need mixed formatting (bold heading + normal body in the same text box) - -For text with mixed formatting, build paragraph by paragraph: - -```bash -# Create empty text box -officecli add slides.pptx /slide[1] --type shape --prop x=2cm --prop y=5cm --prop width=20cm --prop height=10cm --prop fill=none - -# Add heading paragraph -officecli add slides.pptx /slide[1]/shape[1] --type paragraph --prop text="Key Metrics" --prop bold=true --prop size=24 --prop color=1E2761 --prop font=Georgia - -# Add body paragraphs -officecli add slides.pptx /slide[1]/shape[1] --type paragraph --prop text="Revenue exceeded targets by 15%" --prop size=16 --prop color=333333 --prop font=Calibri --prop list=bullet - -officecli add slides.pptx /slide[1]/shape[1] --type paragraph --prop text="Customer satisfaction at all-time high" --prop size=16 --prop color=333333 --prop font=Calibri --prop list=bullet -``` - -For inline formatting within a paragraph, use runs: - -```bash -# Add run with different styling to existing paragraph -officecli add slides.pptx /slide[1]/shape[1]/paragraph[1] --type run --prop text=" (verified)" --prop italic=true --prop color=888888 --prop size=12 -``` - -### Pictures & Images - -```bash -# Local file -officecli add slides.pptx /slide[1] --type picture --prop path=photo.jpg --prop x=2cm --prop y=4cm --prop width=14cm --prop height=10cm --prop alt="Team photo" - -# HTTP URL -officecli add slides.pptx /slide[1] --type picture --prop path=https://example.com/logo.png --prop x=28cm --prop y=16cm --prop width=4cm --prop height=2cm --prop alt="Company logo" - -# Base64 data URI -officecli add slides.pptx /slide[1] --type picture --prop "path=data:image/png;base64,iVBORw0KGgo..." --prop width=10cm --prop height=8cm - -# Clipped to circle (for avatars) -officecli add slides.pptx /slide[1] --type picture --prop path=avatar.jpg --prop geometry=ellipse --prop width=5cm --prop height=5cm --prop alt="Profile photo" - -# Clipped to rounded rectangle -officecli add slides.pptx /slide[1] --type picture --prop path=screenshot.png --prop shape=roundRect --prop x=2cm --prop y=4cm --prop width=14cm --prop height=10cm - -# SVG image (native support, no rasterization needed) -officecli add slides.pptx /slide[1] --type picture --prop path=icon.svg --prop x=2cm --prop y=2cm --prop width=2cm --prop height=2cm --prop alt="Settings icon" -``` - -Supported formats: png, jpg, gif, bmp, tiff, emf, wmf, svg. HTTP URLs have 30s timeout. - -**SVG viewport note:** The SVG renderer uses a fixed viewport. For wide slides (33.87cm), content near the right edge may appear clipped in SVG output. This is a render artifact — the actual PPTX layout is correct. Do not move elements left to "fix" SVG clipping; verify actual positions with `officecli validate` and by opening the file. - -#### Picture Cropping - -```bash -# Crop all sides equally (percentage 0-100) -officecli set slides.pptx /slide[1]/picture[1] --prop crop=10 - -# Crop individual sides -officecli set slides.pptx /slide[1]/picture[1] --prop cropLeft=10 --prop cropRight=10 --prop cropTop=5 --prop cropBottom=5 -``` - -#### Shape Image Fill - -Fill any shape with an image (useful for textured backgrounds or image-masked shapes): - -```bash -officecli set slides.pptx /slide[1]/shape[1] --prop image=texture.jpg -``` - -### Charts - -```bash -# Column chart -officecli add slides.pptx /slide[1] --type chart --prop chartType=column --prop title="Sales" --prop categories="Q1,Q2,Q3,Q4" --prop series1="2024:100,200,150,300" --prop series2="2025:120,250,180,350" --prop x=2cm --prop y=4cm --prop width=20cm --prop height=12cm --prop colors=1E2761,CADCFC - -# Pie chart -officecli add slides.pptx /slide[1] --type chart --prop chartType=pie --prop title="Market Share" --prop categories="Product A,Product B,Product C" --prop data="Share:40,35,25" --prop colors=1E2761,CADCFC,F5F5F5 --prop dataLabels=percent --prop legend=right - -# Line chart -officecli add slides.pptx /slide[1] --type chart --prop chartType=line --prop title="Trend" --prop categories="Jan,Feb,Mar,Apr,May,Jun" --prop series1="Revenue:10,15,13,20,22,28" --prop axisTitle="USD (M)" --prop catTitle="Month" --prop legend=bottom --prop marker="circle:6:1E2761" - -# Stacked bar -officecli add slides.pptx /slide[1] --type chart --prop chartType=barStacked --prop categories="US,EU,APAC" --prop series1="Product A:30,40,25" --prop series2="Product B:20,35,40" --prop colors=1E2761,CADCFC --prop legend=bottom - -# Doughnut -officecli add slides.pptx /slide[1] --type chart --prop chartType=doughnut --prop categories="Complete,Remaining" --prop data="Progress:75,25" --prop colors=2C5F2D,E8E8E8 - -# Combo chart (bar + line) -officecli add slides.pptx /slide[1] --type chart --prop chartType=combo --prop categories="Q1,Q2,Q3,Q4" --prop series1="Revenue:100,200,150,300" --prop series2="Growth:10,15,12,25" --prop comboSplit=1 --prop secondary=2 --prop colors=1E2761,F96167 - -# Radar/spider chart -officecli add slides.pptx /slide[1] --type chart --prop chartType=radar --prop categories="Quality,Speed,Cost,Innovation,Support" --prop data="Score:8,7,6,9,8" -``` - -**Chart types:** column, columnStacked, columnPercentStacked, column3d, bar, barStacked, barPercentStacked, bar3d, line, lineStacked, line3d, pie, pie3d, doughnut, area, areaStacked, area3d, scatter, bubble, radar, stock, combo - -#### Modern Chart Styling Recipe - -Default charts look dated. Apply these properties for a clean, modern look: - -```bash -officecli add slides.pptx /slide[1] --type chart \ - --prop chartType=column \ - --prop categories="Q1,Q2,Q3,Q4" \ - --prop series1="Revenue:42,58,65,78" \ - --prop x=2cm --prop y=4cm --prop width=29cm --prop height=13cm \ - --prop colors=1E2761,CADCFC \ - --prop plotFill=none \ - --prop chartFill=none \ - --prop gridlines="E2E8F0:0.5" \ - --prop dataLabels=value \ - --prop labelPos=outsideEnd \ - --prop labelFont="10:64748B:false" \ - --prop axisFont="10:64748B:Calibri" \ - --prop legendFont="10:64748B:Calibri" \ - --prop title.font=Georgia \ - --prop title.size=14 \ - --prop title.color=333333 \ - --prop series.outline="FFFFFF-0.5" \ - --prop legend=bottom -``` - -Key styling properties: -- `plotFill=none` and `chartFill=none` -- clean transparent background -- `gridlines="E2E8F0:0.5"` -- subtle, light gridlines -- `series.outline="FFFFFF-0.5"` -- thin white border between bars -- `axisFont` and `legendFont` -- muted gray labels -- `gap=80` -- comfortable spacing between bar groups - -**Note:** `gap`/`gapwidth` is ignored during `add` -- apply it separately after creation: - -```bash -officecli set slides.pptx "/slide[1]/chart[1]" --prop gap=80 -``` - -#### 深色背景图表配方(Dark Slide + Chart) - -> [!CAUTION] -> **当 slide 背景为深色(如 `1E2761`、`36454F` 等)时,图表默认使用黑色文字,在深色背景上几乎不可见。** -> 必须手动设置轴标签、图例、标题颜色为浅色。 - -```bash -# 深色背景 slide 上的图表 — 必须设置以下颜色属性 -officecli add slides.pptx /slide[N] --type chart \ - --prop chartType=column \ - --prop categories="Q1,Q2,Q3,Q4" \ - --prop series1="Revenue:42,58,65,78" \ - --prop x=2cm --prop y=4cm --prop width=29cm --prop height=13cm \ - --prop colors=CADCFC,97BC62 \ - --prop plotFill=none \ - --prop chartFill=none \ - --prop gridlines="FFFFFF:0.2" \ - --prop axisFont="11:CADCFC:Calibri" \ - --prop legendFont="11:CADCFC:Calibri" \ - --prop labelFont="10:FFFFFF:false" \ - --prop title.color=FFFFFF \ - --prop title.size=14 -``` - -深色背景图表配色规则: -- `axisFont` 和 `legendFont`:使用浅色(如 `CADCFC`、`FFFFFF`) -- `labelFont`:使用白色或浅色 -- `title.color`:使用白色 -- `gridlines`:使用低透明度白色(如 `FFFFFF:0.2`)而非深色 -- 图表 `series` 颜色:避免深色(如 `1E2761`);改用高亮色(如 `CADCFC`、`97BC62`) - -**颜色对齐提示**:officecli 默认使用系统图表颜色(蓝/橙/灰),与自定义 deck 调色板不符。添加图表后,`colors` prop 指定系列颜色以匹配 deck 主题色: -```bash -# 与 Midnight Executive 调色板对齐 ---prop colors=CADCFC,1E2761,8899BB -``` - -> **多系列图表量级差异警告**:若同一图表中各系列数值差异超过 5x(如 8,000,000 vs 400,000), -> 小值系列在默认 Y 轴下几乎不可见。解决方案(按推荐顺序): -> 1. 拆分为两张独立图表 -> 2. 将最大绝对值系列改为大字号 KPI callout,其余系列单独图表 -> 3. 在图表说明中标注"图表显示相对比例,绝对值见 KPI callout" - -#### Multi-Series Column Chart - -Include all series in the `add` command using `series1`, `series2`, etc. or the `data` prop. Both forms work in single commands and in batch mode: - -```bash -# Using seriesN props -officecli add slides.pptx "/slide[1]" --type chart --prop chartType=column \ - --prop categories="Q1,Q2,Q3,Q4" \ - --prop series1="2024:42,58,65,78" \ - --prop series2="2025:51,67,74,92" \ - --prop x=2cm --prop y=4cm --prop width=29cm --prop height=13cm \ - --prop colors=1E2761,CADCFC - -# Or using data prop (equivalent) -officecli add slides.pptx "/slide[1]" --type chart --prop chartType=column \ - --prop categories="Q1,Q2,Q3,Q4" \ - --prop data="2024:42,58,65,78;2025:51,67,74,92" \ - --prop x=2cm --prop y=4cm --prop width=29cm --prop height=13cm \ - --prop colors=1E2761,CADCFC -``` - -Batch mode: - -```bash -officecli add slides.pptx /slide[1] --type chart --prop chartType=column --prop categories="Q1,Q2,Q3,Q4" --prop series1="2024:42,58,65,78" --prop series2="2025:51,67,74,92" --prop x=2cm --prop y=4cm --prop width=29cm --prop height=13cm --prop colors="1E2761,CADCFC" -``` - -**Important:** Once a chart is created, `set --prop data=` and `set --prop seriesN=` can only update existing series -- they cannot add new series. To add series to an existing chart, delete it and recreate with all series in the `add` command. See [editing.md](editing.md#update-charts) for the delete-and-recreate pattern. - -### Tables - -```bash -# Create table -officecli add slides.pptx /slide[1] --type table --prop rows=4 --prop cols=3 --prop x=2cm --prop y=5cm --prop width=29cm --prop height=12cm - -# Style header row -officecli set slides.pptx /slide[1]/table[1]/tr[1] --prop c1="Metric" --prop c2="Q3" --prop c3="Q4" --prop bold=true --prop fill=1E2761 --prop color=FFFFFF - -# Fill data rows -officecli set slides.pptx /slide[1]/table[1]/tr[2] --prop c1="Revenue" --prop c2="$4.2M" --prop c3="$5.1M" -officecli set slides.pptx /slide[1]/table[1]/tr[3] --prop c1="Users" --prop c2="12,400" --prop c3="15,800" -officecli set slides.pptx /slide[1]/table[1]/tr[4] --prop c1="NPS" --prop c2="72" --prop c3="81" - -# Apply table style -officecli set slides.pptx /slide[1]/table[1] --prop style=medium2 - -# Cell-level styling -officecli set slides.pptx /slide[1]/table[1]/tr[2]/tc[3] --prop bold=true --prop color=2C5F2D - -# Set font size on all cells in a row (default is 18pt -- often too large for data tables) -officecli set slides.pptx /slide[1]/table[1]/tr[2] --prop size=12 --prop font=Calibri - -# Set font size on a single cell -officecli set slides.pptx /slide[1]/table[1]/tr[2]/tc[1] --prop size=11 - -# Set font size on the entire table at once (cascades to all cells) -officecli set slides.pptx /slide[1]/table[1] --prop size=12 --prop font=Calibri - -# Merge cells -officecli set slides.pptx /slide[1]/table[1]/tr[1]/tc[1] --prop merge.right=2 -``` - -**Table font size tip:** The default table font size is 18pt, which is too large for most data tables. Set `size=11` or `size=12` on the table after creation. **Important ordering:** Set table-level `size`/`font` **after** populating all row content. Row-level `set` commands (e.g., `set tr[1] --prop c1=...`) reset font properties on that row to defaults, overwriting any prior table-level cascade. The correct order is: (1) create table, (2) populate all rows, (3) set table-level `size`/`font`. Alternatively, include `size`/`font` in each row-level `set` command. - -**Table border cascading:** Setting `border*`, `text`, `bold`, `italic`, `size`, `font`, `color`, `underline`, `strike`, `valign`, `fill` on the table path cascades to all cells. This is useful for applying uniform borders across the entire table: - -```bash -# Apply uniform borders to all cells -officecli set slides.pptx /slide[1]/table[1] --prop border=CCCCCC - -# Individual cell borders -officecli set slides.pptx /slide[1]/table[1]/tr[1]/tc[1] --prop border.bottom=1E2761 -``` - -Table style presets: medium1, medium2, medium3, medium4, light1, light2, light3, dark1, dark2, none - -### Connectors & Arrows - -```bash -# Horizontal line -officecli add slides.pptx /slide[1] --type connector --prop x=2cm --prop y=10cm --prop width=29cm --prop height=0 --prop line=CCCCCC --prop lineWidth=1pt - -# Arrow -officecli add slides.pptx /slide[1] --type connector --prop x=5cm --prop y=8cm --prop width=10cm --prop height=0 --prop tailEnd=triangle --prop line=1E2761 --prop lineWidth=2pt - -# Curved connector between shapes -officecli add slides.pptx /slide[1] --type connector --prop preset=curve --prop startShape=1 --prop endShape=2 --prop line=4472C4 --prop lineWidth=1.5pt - -# Dashed line -officecli add slides.pptx /slide[1] --type connector --prop x=2cm --prop y=5cm --prop width=10cm --prop height=0 --prop lineDash=dash --prop line=999999 -``` - -Connector presets: straight, elbow, curve. -Arrow types: none, triangle, stealth, diamond, oval, arrow. - -### Speaker Notes - -```bash -officecli add slides.pptx /slide[1] --type notes --prop text="Key talking point: emphasize the 25% growth figure" - -# Or set notes on existing slide -officecli set slides.pptx /slide[1]/notes --prop text="Discuss quarterly trends and outlook" -``` - ---- - -## Advanced Features - -### Gradient Backgrounds - -```bash -# Linear gradient (two colors) -officecli set slides.pptx /slide[1] --prop "background=1E2761-0D1433" - -# Linear with angle -officecli set slides.pptx /slide[1] --prop "background=1E2761-0D1433-45" - -# Three-color gradient -officecli set slides.pptx /slide[1] --prop "background=1E2761-4472C4-CADCFC" - -# Radial gradient -officecli set slides.pptx /slide[1] --prop "background=radial:FFFFFF-1E2761-center" - -# Custom stop positions -officecli set slides.pptx /slide[1] --prop "background=1E2761@0-4472C4@70-CADCFC@100" - -# Image background -officecli set slides.pptx /slide[1] --prop background=image:bg.jpg -``` - -### Animations - -Format: `EFFECT[-CLASS][-DIRECTION][-DURATION][-TRIGGER][-delay=N][-easein=N][-easeout=N]` - -Segments after the effect name are identified by content, not position, so they can appear in flexible order. - -```bash -# Fade entrance -officecli set slides.pptx /slide[1]/shape[1] --prop animation=fade - -# Fly in from left -officecli set slides.pptx /slide[1]/shape[2] --prop animation=fly-entrance-left-400-after - -# Zoom exit -officecli set slides.pptx /slide[1]/shape[3] --prop animation=zoom-exit-500-with - -# With delay and easing -officecli set slides.pptx /slide[1]/shape[4] --prop animation=fade-entrance-600-after-delay=200-easein=50 - -# Remove animation -officecli set slides.pptx /slide[1]/shape[1] --prop animation=none -``` - -Effects: appear, fade, fly, zoom, wipe, bounce, float, split, wheel, spin, grow, swivel, checkerboard, blinds, bars, box, circle, diamond, dissolve, flash, plus, random, strips, wedge, bold, wave, crawl, swipe - -Classes: entrance (default), exit, emphasis - -Triggers: click (default for 1st on slide), after (default for subsequent), with - -**Timing guidance:** -- Entrance animations: 300-500ms (fast enough to not feel sluggish) -- Emphasis: 600-800ms -- Sequential element reveals: use `after` trigger with 100-200ms delay between elements -- Avoid animating every element -- reserve animations for key data points and section reveals. - -### Transitions - -```bash -# Basic transitions -officecli set slides.pptx /slide[1] --prop transition=fade -officecli set slides.pptx /slide[2] --prop transition=push-left -officecli set slides.pptx /slide[3] --prop transition=wipe-right-slow - -# Morph transitions (shapes with same name animate between slides) -officecli set slides.pptx /slide[2] --prop transition=morph -officecli set slides.pptx /slide[2] --prop transition=morph-byWord -officecli set slides.pptx /slide[2] --prop transition=morph-byChar - -# With auto-advance -officecli set slides.pptx /slide[1] --prop transition=fade --prop advanceTime=3000 --prop advanceClick=true -``` - -Morph transitions automatically add `!!` prefix to shape names for cross-slide matching. Give shapes the same name on consecutive slides to pair them for morph animation. **When editing templates:** Shapes with `!!`-prefixed names (e.g., `!!bar1`, `!!dot3`) are decorative elements used for morph transitions. Leave them in place -- removing or renaming them breaks the animation. These shapes may be positioned off-screen (x>33cm) to morph-in on transition. - -**Recommended pairings:** -- Title/closing slides: `fade` (clean, professional) -- Content slides: `push-left` or `wipe-right` (directional flow) -- Section dividers: `fade` or `dissolve` (signals topic change) -- Data/chart slides: `fade` (don't distract from data) -- Avoid using more than 2-3 different transition types per deck. - -### 3D Effects - -```bash -# 3D rotation -officecli set slides.pptx /slide[1]/shape[1] --prop "rot3d=15,30,0" - -# Bevel -officecli set slides.pptx /slide[1]/shape[1] --prop bevel=circle --prop depth=5 --prop material=plastic --prop lighting=balanced - -# Individual rotation axes -officecli set slides.pptx /slide[1]/shape[1] --prop rotX=20 --prop rotY=30 - -# Bevel with custom dimensions -officecli set slides.pptx /slide[1]/shape[1] --prop bevel=relaxedInset-8-4 -``` - -Bevel presets: circle, relaxedInset, cross, coolSlant, angle, softRound, convex, slope, divot, riblet, hardEdge, artDeco - -Materials: plastic, metal, warmMatte, matte, flat, clear, softMetal, powder, translucentPowder, darkEdge - -Lighting: threePt, balanced, soft, harsh, flood, contrasting, morning, sunrise, sunset, flat, glow, brightRoom - -### Motion Paths - -```bash -# Diagonal motion path -officecli set slides.pptx /slide[1]/shape[1] --prop "motionPath=M 0.0 0.0 L 1.0 1.0 E-500-click" - -# Remove motion path -officecli set slides.pptx /slide[1]/shape[1] --prop motionPath=none -``` - -Coordinates are normalized 0.0-1.0 relative to slide dimensions. - -### Custom Geometry - -```bash -# Triangle (coordinates in 0-100 relative space) -officecli add slides.pptx /slide[1] --type shape --prop "geometry=M 0,100 L 50,0 L 100,100 Z" --prop fill=4472C4 --prop x=5cm --prop y=5cm --prop width=10cm --prop height=8cm - -# Custom arrow -officecli add slides.pptx /slide[1] --type shape --prop "geometry=M 0,40 L 60,40 L 60,0 L 100,50 L 60,100 L 60,60 L 0,60 Z" --prop fill=1E2761 --prop x=5cm --prop y=5cm --prop width=8cm --prop height=4cm -``` - -Syntax: M=moveTo, L=lineTo, C=cubicBezier, Q=quadBezier, Z=close. Coordinate space is 0-100. - -### Text Effects - -```bash -# Text shadow (on shapes with fill=none) -officecli add slides.pptx /slide[1] --type shape --prop text="Shadow Text" --prop fill=none --prop shadow="000000-4-135-2-50" --prop size=36 --prop bold=true - -# Text glow -officecli add slides.pptx /slide[1] --type shape --prop text="Glow Text" --prop fill=none --prop glow="FF0000-8-75" --prop size=36 - -# Text gradient fill -officecli add slides.pptx /slide[1] --type shape --prop text="Gradient Text" --prop fill=none --prop textFill=FF0000-0000FF --prop size=48 --prop bold=true - -# WordArt / text warp -officecli set slides.pptx /slide[1]/shape[1] --prop textWarp=textWave1 -``` - -Shadow format: `COLOR-BLUR-ANGLE-DIST-OPACITY` (e.g., "000000-6-135-4-60") -Glow format: `COLOR-RADIUS-OPACITY` (e.g., "FF0000-8-75") - -Note: When a shape has `fill=none`, shadow/glow/reflection/softEdge apply to text runs instead of the shape itself. - ---- - -## Multi-Shape Slide Recipes - -### Complete Title Slide - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=1E2761 -officecli add slides.pptx /slide[1] --type shape --prop text="Quarterly Review" --prop x=2cm --prop y=5cm --prop width=29.87cm --prop height=4cm --prop font=Georgia --prop size=44 --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=none -officecli add slides.pptx /slide[1] --type shape --prop text="Q4 2025 | Finance Division" --prop x=2cm --prop y=10cm --prop width=29.87cm --prop height=2cm --prop font=Calibri --prop size=20 --prop color=CADCFC --prop align=center --prop fill=none -officecli add slides.pptx /slide[1] --type shape --prop text="December 2025" --prop x=2cm --prop y=16cm --prop width=29.87cm --prop height=1.5cm --prop font=Calibri --prop size=12 --prop color=8899BB --prop align=center --prop fill=none -``` - -### Complete Stats Slide - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=1E2761 -officecli add slides.pptx /slide[2] --type shape --prop text="By The Numbers" --prop x=2cm --prop y=1cm --prop width=29.87cm --prop height=2.5cm --prop font=Georgia --prop size=32 --prop bold=true --prop color=FFFFFF --prop align=center --prop fill=none -officecli add slides.pptx /slide[2] --type shape --prop text="25%" --prop x=2cm --prop y=5cm --prop width=9cm --prop height=4cm --prop font=Georgia --prop size=64 --prop bold=true --prop color=CADCFC --prop align=center --prop valign=bottom --prop fill=none -officecli add slides.pptx /slide[2] --type shape --prop text="Revenue Growth" --prop x=2cm --prop y=9.5cm --prop width=9cm --prop height=2cm --prop font=Calibri --prop size=14 --prop color=8899BB --prop align=center --prop fill=none -officecli add slides.pptx /slide[2] --type shape --prop text="94%" --prop x=12.5cm --prop y=5cm --prop width=9cm --prop height=4cm --prop font=Georgia --prop size=64 --prop bold=true --prop color=CADCFC --prop align=center --prop valign=bottom --prop fill=none -officecli add slides.pptx /slide[2] --type shape --prop text="Customer Retention" --prop x=12.5cm --prop y=9.5cm --prop width=9cm --prop height=2cm --prop font=Calibri --prop size=14 --prop color=8899BB --prop align=center --prop fill=none -officecli add slides.pptx /slide[2] --type shape --prop text="1.2M" --prop x=23cm --prop y=5cm --prop width=9cm --prop height=4cm --prop font=Georgia --prop size=64 --prop bold=true --prop color=CADCFC --prop align=center --prop valign=bottom --prop fill=none -officecli add slides.pptx /slide[2] --type shape --prop text="Active Users" --prop x=23cm --prop y=9.5cm --prop width=9cm --prop height=2cm --prop font=Calibri --prop size=14 --prop color=8899BB --prop align=center --prop fill=none -``` - -### Table + Chart Slide - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=FFFFFF -officecli add slides.pptx /slide[3] --type shape --prop text="Performance Dashboard" --prop x=2cm --prop y=0.5cm --prop width=29.87cm --prop height=2.5cm --prop font=Georgia --prop size=28 --prop bold=true --prop color=1E2761 --prop align=left --prop fill=none -officecli add slides.pptx /slide[3] --type chart --prop chartType=column --prop categories="Q1,Q2,Q3,Q4" --prop series1="Revenue:42,58,65,78" --prop x=2cm --prop y=3.5cm --prop width=14cm --prop height=8cm --prop colors=1E2761 --prop plotFill=none --prop chartFill=none --prop gridlines="E2E8F0:0.5" --prop legend=none -officecli add slides.pptx /slide[3] --type table --prop rows=3 --prop cols=4 --prop x=18cm --prop y=3.5cm --prop width=14cm --prop height=8cm -officecli set slides.pptx "/slide[3]/table[1]/tr[1]" --prop c1=Metric --prop c2=Q2 --prop c3=Q3 --prop c4=Q4 --prop bold=true --prop fill=1E2761 --prop color=FFFFFF -officecli set slides.pptx "/slide[3]/table[1]/tr[2]" --prop c1=Revenue --prop c2='$58M' --prop c3='$65M' --prop c4='$78M' -officecli set slides.pptx "/slide[3]/table[1]/tr[3]" --prop c1=Growth --prop c2=12% --prop c3=15% --prop c4=20% -``` - -### Timeline / Roadmap - -Horizontal timeline with milestone circles and alternating above/below labels. - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=FFFFFF -officecli add slides.pptx /slide[N] --type shape --prop text="Milestones & Roadmap" --prop x=2cm --prop y=1cm --prop width=29.87cm --prop height=2.5cm --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 --prop align=left --prop fill=none -officecli add slides.pptx /slide[N] --type connector --prop x=2cm --prop y=10cm --prop width=29.87cm --prop height=0 --prop line=CADCFC --prop lineWidth=2pt -officecli add slides.pptx /slide[N] --type shape --prop preset=ellipse --prop fill=1E2761 --prop x=4cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop line=none -officecli add slides.pptx /slide[N] --type shape --prop text=Q1 --prop x=4cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop fill=none --prop color=FFFFFF --prop size=16 --prop bold=true --prop align=center --prop valign=center -officecli add slides.pptx /slide[N] --type shape --prop text="Product Launch" --prop x=2.5cm --prop y=5.5cm --prop width=6cm --prop height=1.5cm --prop fill=none --prop font=Calibri --prop size=14 --prop bold=true --prop color=1E2761 --prop align=center -officecli add slides.pptx /slide[N] --type shape --prop text="iOS & Android app -50K users target" --prop x=2.5cm --prop y=7cm --prop width=6cm --prop height=1.5cm --prop fill=none --prop font=Calibri --prop size=11 --prop color=8899BB --prop align=center -officecli add slides.pptx /slide[N] --type shape --prop preset=ellipse --prop fill=CADCFC --prop x=12cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop line=none -officecli add slides.pptx /slide[N] --type shape --prop text=Q2 --prop x=12cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop fill=none --prop color=1E2761 --prop size=16 --prop bold=true --prop align=center --prop valign=center -officecli add slides.pptx /slide[N] --type shape --prop text="B2B Launch" --prop x=10.5cm --prop y=12cm --prop width=6cm --prop height=1.5cm --prop fill=none --prop font=Calibri --prop size=14 --prop bold=true --prop color=1E2761 --prop align=center -officecli add slides.pptx /slide[N] --type shape --prop text="Enterprise platform -10 pilot customers" --prop x=10.5cm --prop y=13.5cm --prop width=6cm --prop height=1.5cm --prop fill=none --prop font=Calibri --prop size=11 --prop color=8899BB --prop align=center -officecli add slides.pptx /slide[N] --type shape --prop preset=ellipse --prop fill=1E2761 --prop x=20cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop line=none -officecli add slides.pptx /slide[N] --type shape --prop text=Q3 --prop x=20cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop fill=none --prop color=FFFFFF --prop size=16 --prop bold=true --prop align=center --prop valign=center -officecli add slides.pptx /slide[N] --type shape --prop text="Scale Phase" --prop x=18.5cm --prop y=5.5cm --prop width=6cm --prop height=1.5cm --prop fill=none --prop font=Calibri --prop size=14 --prop bold=true --prop color=1E2761 --prop align=center -officecli add slides.pptx /slide[N] --type shape --prop text="500K users -EU expansion" --prop x=18.5cm --prop y=7cm --prop width=6cm --prop height=1.5cm --prop fill=none --prop font=Calibri --prop size=11 --prop color=8899BB --prop align=center -officecli add slides.pptx /slide[N] --type shape --prop preset=ellipse --prop fill=CADCFC --prop x=28cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop line=none -officecli add slides.pptx /slide[N] --type shape --prop text=Q4 --prop x=28cm --prop y=8.5cm --prop width=3cm --prop height=3cm --prop fill=none --prop color=1E2761 --prop size=16 --prop bold=true --prop align=center --prop valign=center -officecli add slides.pptx /slide[N] --type shape --prop text=Profitability --prop x=26.5cm --prop y=12cm --prop width=6cm --prop height=1.5cm --prop fill=none --prop font=Calibri --prop size=14 --prop bold=true --prop color=1E2761 --prop align=center -officecli add slides.pptx /slide[N] --type shape --prop text="2M users -Break-even" --prop x=26.5cm --prop y=13.5cm --prop width=6cm --prop height=1.5cm --prop fill=none --prop font=Calibri --prop size=11 --prop color=8899BB --prop align=center -``` - -Pattern: **The horizontal connector is required** -- it is the visual spine that connects milestone circles. Without it, the milestones appear as disconnected circles. Place it at the vertical center of the milestone circles. Circle nodes at even intervals. Odd milestones labeled above the line, even milestones below. Alternating primary/secondary fill colors on circles for visual rhythm. Replace `/slide[N]` with the actual slide index. - -**Vertical connectors:** To draw a vertical line (e.g., connecting flow boxes top-to-bottom), use `width=0` and set `height` to the desired length. The same `line` and `lineWidth` props apply: - -```bash -officecli add slides.pptx /slide[N] --type connector --prop x=10cm --prop y=5cm --prop width=0 --prop height=2cm --prop line=CADCFC --prop lineWidth=2pt -``` - -### Conversion Funnel - -Decreasing-width trapezoids stacked vertically with centered labels. - -```bash -officecli add slides.pptx / --type slide --prop layout=blank --prop background=FFFFFF -officecli add slides.pptx /slide[N] --type shape --prop text="Conversion Funnel" --prop x=2cm --prop y=0.5cm --prop width=29.87cm --prop height=2.5cm --prop font=Georgia --prop size=36 --prop bold=true --prop color=1E2761 --prop align=left --prop fill=none -officecli add slides.pptx /slide[N] --type shape --prop preset=trapezoid --prop text="Visitors: 48.2M" --prop x=4cm --prop y=3.5cm --prop width=26cm --prop height=2.5cm --prop fill=1E2761 --prop color=FFFFFF --prop size=16 --prop bold=true --prop align=center --prop valign=center -officecli add slides.pptx /slide[N] --type shape --prop preset=trapezoid --prop text="Product Views: 28.9M (60%)" --prop x=6.5cm --prop y=6.5cm --prop width=21cm --prop height=2.5cm --prop fill=2C5F2D --prop color=FFFFFF --prop size=16 --prop bold=true --prop align=center --prop valign=center -officecli add slides.pptx /slide[N] --type shape --prop preset=trapezoid --prop text="Add to Cart: 8.7M (18%)" --prop x=9cm --prop y=9.5cm --prop width=16cm --prop height=2.5cm --prop fill=97BC62 --prop color=2D2D2D --prop size=16 --prop bold=true --prop align=center --prop valign=center -officecli add slides.pptx /slide[N] --type shape --prop preset=trapezoid --prop text="Checkout: 3.5M (7.3%)" --prop x=11cm --prop y=12.5cm --prop width=12cm --prop height=2.5cm --prop fill=D4A843 --prop color=FFFFFF --prop size=16 --prop bold=true --prop align=center --prop valign=center -officecli add slides.pptx /slide[N] --type shape --prop preset=trapezoid --prop text="Purchase: 2.2M (4.6%)" --prop x=13cm --prop y=15.5cm --prop width=8cm --prop height=2.5cm --prop fill=8B6B00 --prop color=FFFFFF --prop size=14 --prop bold=true --prop align=center --prop valign=center -officecli add slides.pptx /slide[N] --type shape --prop text="Drop-off Rates" --prop x=27cm --prop y=3.5cm --prop width=5cm --prop height=1.5cm --prop fill=none --prop font=Calibri --prop size=14 --prop bold=true --prop color=333333 --prop align=left -officecli add slides.pptx /slide[N] --type shape --prop text=-40% --prop x=27cm --prop y=6.5cm --prop width=5cm --prop height=1cm --prop fill=none --prop font=Calibri --prop size=12 --prop color=B85042 --prop align=left -officecli add slides.pptx /slide[N] --type shape --prop text=-70% --prop x=27cm --prop y=9.5cm --prop width=5cm --prop height=1cm --prop fill=none --prop font=Calibri --prop size=12 --prop color=B85042 --prop align=left -officecli add slides.pptx /slide[N] --type shape --prop text=-60% --prop x=27cm --prop y=12.5cm --prop width=5cm --prop height=1cm --prop fill=none --prop font=Calibri --prop size=12 --prop color=B85042 --prop align=left -``` - -Pattern: Each trapezoid is progressively narrower (x inset increases, width decreases). Color gradient from dark to light communicates volume reduction. Drop-off rate annotations in a column on the right. Replace `/slide[N]` with the actual slide index. - ---- - -## Other Element Types - -### Video / Audio - -```bash -# Embed video with autoplay -officecli add slides.pptx /slide[1] --type video --prop path=demo.mp4 --prop x=3cm --prop y=3cm --prop width=18cm --prop height=10cm --prop autoplay=true - -# Background audio -officecli add slides.pptx /slide[1] --type audio --prop path=bgm.mp3 --prop volume=50 --prop autoplay=true -``` - -### Equations - -```bash -officecli add slides.pptx /slide[1] --type equation --prop "formula=E = mc^2" --prop x=5cm --prop y=10cm -officecli add slides.pptx /slide[1] --type equation --prop "formula=x = (-b +/- sqrt(b^2 - 4ac)) / 2a" --prop x=3cm --prop y=5cm --prop width=8cm --prop height=2cm -``` - -### Slide Zoom - -```bash -# Add zoom element linking to slide 3 -officecli add slides.pptx /slide[1] --type zoom --prop target=3 --prop x=2cm --prop y=3cm --prop returntoparent=true -``` - -### 3D Models - -```bash -# Insert .glb 3D model -officecli add slides.pptx /slide[1] --type 3dmodel --prop path=model.glb --prop x=5cm --prop y=3cm --prop width=12cm --prop height=12cm --prop rotx=30 --prop roty=45 -``` - -### Groups - -```bash -# Group shapes 1, 2, and 3 together -officecli add slides.pptx /slide[1] --type group --prop shapes=1,2,3 --prop name="CardGroup" -``` - ---- - -## Final QA & Delivery Checklist - -在执行 `officecli close` 和 `officecli validate` 前,逐项确认: - -### 溢出检查(必做,每张 slide) - -标准 widescreen:**宽 33.87cm × 高 19.05cm** - -```bash -# 检查单张 slide 的所有元素位置 -officecli get slides.pptx '/slide[N]' --depth 1 -``` - -对每个 shape,手动验证: -- `y + height ≤ 19.05cm`(底边不溢出) -- `x + width ≤ 33.87cm`(右边不溢出) - -如有溢出:优先缩小字号或减少内容行数;最后手段才是调整起始位置。 - -**多卡片布局专项检查**:对包含 step cards、feature grids、before/after 卡片、timeline nodes 的 slide,必须逐张卡片单独检查(不得估算)。使用 `officecli get slides.pptx '/slide[N]/shape[M]'` 对每张卡片的 shape 逐一确认 `y + height ≤ 19.05cm`。 - -### Agenda 一致性检查(有 Agenda slide 时必做) - -如演示文稿包含 Agenda/TOC/目录 slide: - -1. 列出实际 slide 结构: - ```bash - officecli view slides.pptx outline - ``` - -2. 对照 Agenda slide 内容,确认: - - [ ] Agenda 列出的每个 section 在 deck 中都有对应 slide - - [ ] deck 中每个主要 section 都被 Agenda 覆盖(无遗漏) - - [ ] section 顺序与 Agenda 顺序一致 - - > **建议**:最后生成 Agenda slide,或在所有 section slide 完成后回来更新 Agenda,避免提前列出未完成的结构。 - -### 视觉元素覆盖检查 - -```bash -officecli view slides.pptx stats -``` - -- 确认 shape count 大于 slide 数量的 2 倍(平均每张 slide ≥ 2 个非文字元素) -- 如发现连续 3 张以上 slide 无色块/图表/图形,回头为其中至少 1 张补充视觉元素 - -### 最终验证 - -```bash -officecli close slides.pptx -officecli validate slides.pptx -``` - -验证通过后,使用 `view html --browser` 进行最终视觉确认。 diff --git a/skills/officecli-pptx/editing.md b/skills/officecli-pptx/editing.md deleted file mode 100644 index ca751deea..000000000 --- a/skills/officecli-pptx/editing.md +++ /dev/null @@ -1,563 +0,0 @@ - - -# Editing Existing Presentations - -Use this guide when modifying an existing .pptx file (template-based or content update). - -## Workflow Overview - -1. **Analyze** the template (structure, layouts, shapes) -2. **Plan** slide mapping (content -> layout) -3. **Structural changes** (clone, remove, reorder slides) -- **do this FIRST** -4. **Content edits** (text, images, charts, tables) -5. **QA** (content + visual + validation) -- see [SKILL.md](SKILL.md#qa-required) - ---- - -## Analyzing the Template - -### Step 1: Structure Overview - -```bash -# See slide count, titles, shape counts -officecli view template.pptx outline -``` - -Output: -``` -File: template.pptx | 8 slides --- Slide 1: "Welcome" - 3 text box(es), 1 picture(s) --- Slide 2: "Agenda" - 2 text box(es) --- Slide 3: "(untitled)" - 4 text box(es), 2 picture(s) -... -``` - -### Step 2: Detailed Shape Inspection - -```bash -# See shape types, fonts, sizes per slide -officecli view template.pptx annotated -``` - -Output: -``` -[/slide[1]] - [Title] "Welcome to Our Company" <- Georgia 44pt - [Text Box] "Q4 2025 Report" <- Calibri 20pt - [Picture] "Logo" <- alt="Company logo" -``` - -**Note:** `view annotated` shows only text-bearing shapes (content shapes), while `get --depth 1` shows ALL shapes including decorative elements (bars, frames, dots). Use `get --depth 1` to see the complete shape list and indices for targeting specific elements. - -> **WARNING: `view annotated` hides non-text shapes.** The annotated view only displays shapes that contain text. Non-text decorative shapes (lines, dots, rectangles without text) are invisible in annotated output. This means shape indices shown in annotated view are NOT contiguous -- there are gaps where non-text shapes exist. **Always use `get --depth 1` to see the true shape indices before targeting shapes by index.** Relying on annotated view alone will cause you to modify the wrong shapes. - -**Template decorative elements:** Many templates contain shapes with `!!`-prefixed names (e.g., `!!bar1`, `!!frame2`). These are decorative elements used for morph transitions between slides. They may be positioned off-screen (x>33cm) to animate into view on slide transition. **Do not rename `!!`-prefixed shapes** -- renaming breaks morph animations. Whether to leave them in place, send them to back, or neutralize them depends on their visual impact on your content. See "Step 2b: Decorative Element Triage" below. - -### Step 2b: Decorative Element Triage - -After identifying all `!!`-prefixed shapes with `get --depth 1`, classify each one: - -**Category A -- Framing elements (leave in place):** -Shapes positioned outside the content zone (x > 30cm, y < 0, or placed at slide edges/corners). These are morph transition elements or border decorations. No action needed. - -**Category B -- Background watermarks (send to back):** -Shapes positioned within the content zone but using light/muted colors (fill or text color with lightness > 80%, e.g., E0E0E0, F2F2F2). These are decorative watermarks. Send them behind content: - -```bash -# Send decorative watermark to back of z-order -officecli set template.pptx "/slide[1]/shape[3]" --prop zorder=back -``` - -**Category C -- Opaque overlays (neutralize):** -Shapes positioned within the content zone with dark/opaque colors (black, dark gray) AND large font sizes (100pt+). These WILL obscure your content. You have two options: - -Option 1 -- Convert to subtle watermark (preserves template design intent): -```bash -# Change giant dark number to light gray watermark and send to back -officecli set template.pptx "/slide[1]/shape[3]" --prop color=E8E8E8 --prop zorder=back -``` - -Option 2 -- Remove entirely (if watermark effect is not desired): -```bash -officecli remove template.pptx "/slide[1]/shape[3]" -``` - -**How to classify:** After running `get --depth 1` on each slide, check each `!!`-prefixed shape: -1. Is it positioned within the content zone (0cm < x < 30cm, 0cm < y < 19cm)? If no -> Category A. -2. Is its text/fill color light (RGB values all > 200)? If yes -> Category B. -3. Is it dark/opaque AND has large font size (>80pt)? -> Category C. - -**Do this BEFORE editing content.** If you skip triage and edit content first, you will not discover the collision until visual QA, at which point you must retroactively fix z-order on every affected slide. - -> **WARNING: Z-order changes cause shape index renumbering.** When you change a shape's z-order (e.g., `zorder=back`), the shape is physically moved in the ShapeTree XML. ALL shape indices shift immediately. After any z-order change, re-query the slide with `get --depth 1` before referencing shapes by index. When changing z-order for multiple shapes on the same slide, **process from highest index to lowest** to minimize cascading index shifts. Combining `zorder` with other properties in a single `set` command is safe -- z-order is applied first, then other properties are applied to the same shape at its new position. - -### Step 3: Per-Slide Deep Inspection - -```bash -# List all shapes on slide 1 with properties -officecli get template.pptx /slide[1] --depth 1 - -# Get specific shape details (position, fill, font, animation) -officecli get template.pptx /slide[1]/shape[1] - -# Get placeholder by type -officecli get template.pptx "/slide[1]/placeholder[title]" - -# Get chart data -officecli get template.pptx /slide[2]/chart[1] - -# Get table structure -officecli get template.pptx /slide[3]/table[1] --depth 3 -``` - -### Step 4: Visual Inspection - -```bash -# HTML preview in browser (recommended -- shows all slides, charts, gradients) -officecli view template.pptx html --browser - -# SVG preview of a single slide (SVG renders only one slide per invocation) -officecli view template.pptx svg --start 2 --end 2 --browser -``` - -### Step 5: Find Specific Elements - -```bash -# Find all pictures without alt text -officecli query template.pptx "picture:no-alt" - -# Find shapes with specific text -officecli query template.pptx 'shape:contains("placeholder")' - -# Find all title shapes -officecli query template.pptx "title" - -# Find shapes on a specific slide -officecli query template.pptx 'slide[2] > shape' -``` - ---- - -## Planning Slide Mapping - -Before making changes, plan which template slides to keep, clone, or remove. - -**Use varied layouts** -- monotonous presentations are a common failure mode. Don't reuse the same layout for every slide. - -Match content type to layout style: -- Key points -> bullet slide -- Team info -> multi-column or icon grid -- Testimonials -> quote/callout slide -- Data -> chart slide or stats callout -- Comparison -> two-column side-by-side -- New section -> section divider slide - -**Example mapping:** -``` -Source content Template slide to use --------------- --------------------- -Title + subtitle -> Slide 1 (title layout) -Executive summary -> Slide 3 (content layout) -Revenue data -> Slide 5 (chart layout) -- clone for each quarter -Team overview -> Slide 4 (multi-column) -- needs 3 members, template has 4 -Conclusion -> Slide 8 (closing layout) - -Action: Remove slides 2, 6, 7. Clone slide 5 twice. Adjust slide 4. -``` - ---- - -## Structural Changes (Do First) - -**Complete ALL structural changes before editing content.** This prevents path index shifts that break subsequent commands. - -### Clone Slides - -```bash -# Clone slide 5 and append to end -officecli add template.pptx / --from /slide[5] - -# Clone slide 3 and insert at position 2 -officecli add template.pptx / --from /slide[3] --index 2 -``` - -### Remove Slides - -```bash -# Remove slide 7 -officecli remove template.pptx /slide[7] - -# Remove slide 3 (indices shift after each removal -- remove from highest index first) -officecli remove template.pptx /slide[3] -``` - -**Remove from highest index to lowest** to avoid index shifting problems. - -### Reorder Slides - -```bash -# Move slide 5 to position index 1 (becomes second slide) -officecli move template.pptx /slide[5] --index 1 -``` - -### Add New Slides - -```bash -# Add blank slide at end -officecli add template.pptx / --type slide --prop layout=blank - -# Add slide with specific layout -officecli add template.pptx / --type slide --prop layout=title - -# Insert at specific position -officecli add template.pptx / --type slide --prop layout=blank --index 3 -``` - -Layout types: title, blank, twoContent, titleOnly, titleContent, section, comparison - ---- - -## Content Editing - -### Modify Text - -**WARNING: Template shapes inherit their original font formatting.** Always include `--prop size=N --prop font=FAMILY --prop color=HEX` when setting text on template shapes. Omitting these causes text to render at the template's original display size (often 40-60pt), producing word-wrapping and overlap. See "Font Cascade from Template Shapes" below. - -```bash -# Replace shape text -officecli set template.pptx /slide[1]/shape[1] --prop text="New Title Text" - -# Set placeholder by type -officecli set template.pptx "/slide[1]/placeholder[title]" --prop text="Q4 Business Review" -officecli set template.pptx "/slide[1]/placeholder[body]" --prop text="Financial highlights and key metrics" - -# Update text with styling -officecli set template.pptx /slide[2]/shape[1] --prop text="Revenue Report" --prop bold=true --prop size=36 --prop color=1E2761 - -# Multi-line text -officecli set template.pptx /slide[3]/shape[2] --prop "text=Point one\\nPoint two\\nPoint three" --prop list=bullet -``` - -### Replace Images - -```bash -# Replace image source -officecli set template.pptx /slide[1]/picture[1] --prop path=new-photo.jpg - -# Update alt text -officecli set template.pptx /slide[1]/picture[1] --prop alt="Updated product photo" - -# Resize and reposition -officecli set template.pptx /slide[2]/picture[1] --prop path=chart-screenshot.png --prop width=14cm --prop height=10cm --prop x=2cm --prop y=5cm -``` - -### Update Charts - -```bash -# Replace chart data (updates values of existing series only) -officecli set template.pptx /slide[3]/chart[1] --prop data="2024:42,58,65,78;2025:51,67,74,92" - -# Update categories -officecli set template.pptx /slide[3]/chart[1] --prop categories="Q1,Q2,Q3,Q4" - -# Update individual series -officecli set template.pptx /slide[3]/chart[1] --prop series1="Revenue:51,67,74,92" - -# Update title and styling -officecli set template.pptx /slide[3]/chart[1] --prop title="Updated Revenue Trend" --prop colors=1E2761,CADCFC -``` - -**`set --prop data=` and `set --prop seriesN=` can only update existing series -- they cannot add new series to a chart.** If you need more series than the chart currently has, you must delete the chart and recreate it with all series included in the `add` command: - -```bash -# Remove existing chart -officecli remove template.pptx "/slide[3]/chart[1]" - -# Recreate with all series (both series1+series2 props and data prop work) -officecli add template.pptx "/slide[3]" --type chart --prop chartType=column \ - --prop categories="Q1,Q2,Q3,Q4" \ - --prop series1="Revenue:51,67,74,92" --prop series2="Costs:30,35,38,42" \ - --prop x=2cm --prop y=4cm --prop width=29cm --prop height=13cm --prop colors=1E2761,CADCFC -``` - -### Update Tables - -**Cascade ordering:** Row-level `set` commands reset font properties (`size`, `font`) to defaults, overwriting prior table-level settings. Apply cell-level overrides **after** row-level defaults. The recommended order: (1) populate row content, (2) set table-level `size`/`font`, (3) apply cell-level overrides last. The default table font size is 18pt -- almost always too large for data tables. - -```bash -# Set cell text via row shorthand -officecli set template.pptx /slide[4]/table[1]/tr[1] --prop c1="Product" --prop c2="Revenue" --prop c3="Growth" - -# Set individual cell -officecli set template.pptx /slide[4]/table[1]/tr[2]/tc[1] --prop text="Widget A" --prop bold=true - -# Style a cell -officecli set template.pptx /slide[4]/table[1]/tr[1]/tc[1] --prop fill=1E2761 --prop color=FFFFFF --prop bold=true --prop align=center - -# Add a new row -officecli add template.pptx /slide[4]/table[1] --type row --prop c1="New Product" --prop c2="$2.1M" --prop c3="+18%" -``` - -### Update Slide Properties - -```bash -# Change background -officecli set template.pptx /slide[1] --prop background=1E2761 - -# Set gradient background -officecli set template.pptx /slide[1] --prop "background=1E2761-0D1433" - -# Add transition -officecli set template.pptx /slide[2] --prop transition=fade - -# Set speaker notes -officecli set template.pptx /slide[1] --prop notes="Opening remarks: welcome the audience" -``` - -### Add New Elements to Existing Slides - -**When adding new slides to an existing template:** Inspect the template's existing slides first (`get /slide[1]`) to match the background color, font family, and color scheme. For layout patterns (stat callouts, charts, card grids, timelines), refer to [creating.md](creating.md) -- adapt its recipes to the template's color palette. - -```bash -# Add text box to slide -officecli add template.pptx /slide[2] --type shape --prop text="Additional Note" --prop x=2cm --prop y=15cm --prop width=20cm --prop height=2cm --prop font=Calibri --prop size=12 --prop color=888888 - -# Add picture to slide -officecli add template.pptx /slide[3] --type picture --prop path=infographic.png --prop x=18cm --prop y=4cm --prop width=13cm --prop height=12cm --prop alt="Q4 infographic" - -# Add chart to slide -officecli add template.pptx /slide[5] --type chart --prop chartType=pie --prop categories="A,B,C" --prop data="Share:40,35,25" --prop x=18cm --prop y=4cm --prop width=12cm --prop height=10cm -``` - -### Remove Elements - -```bash -# Remove a shape -officecli remove template.pptx /slide[3]/shape[4] - -# Remove a picture -officecli remove template.pptx /slide[2]/picture[2] - -# Remove a chart -officecli remove template.pptx /slide[5]/chart[1] -``` - ---- - -## Template Adaptation Pitfalls - -### Dark Background Table Text Color - -Tables default to black text. On dark backgrounds (navy, charcoal, etc.), table content will be invisible unless you explicitly set `color` to a light value. After creating or populating a table on a dark slide, set the text color on the table element: - -```bash -# Set white text on entire table (cascades to all cells) -officecli set template.pptx "/slide[5]/table[1]" --prop color=FFFFFF - -# Or set per-row when populating -officecli set template.pptx "/slide[5]/table[1]/tr[1]" --prop c1="Metric" --prop c2="Value" --prop color=FFFFFF -``` - -This applies to any slide where the background color is dark -- whether from the original template or from a `background=` you set on a new slide. - -### Chart Colors on Dark Backgrounds - -Charts default to colors that may be invisible on dark slide backgrounds. When placing a chart on a dark background slide, explicitly set `colors` using the template's light/accent palette values: - -```bash -# BAD -- default colors invisible on navy background -officecli add template.pptx "/slide[6]" --type chart --prop chartType=column ... - -# GOOD -- explicit light colors for dark background -officecli add template.pptx "/slide[6]" --type chart --prop chartType=column \ - --prop colors=CADCFC,D4A843 \ - --prop axisFont="10:AABBCC:Calibri" \ - --prop legendFont="10:AABBCC:Calibri" \ - --prop gridlines="334466:0.5" \ - --prop plotFill=none --prop chartFill=none ... -``` - -Use the template's secondary or accent colors (not the primary dark color) for chart series. Set axis and legend fonts to a light/muted color. Set gridlines to a subtle mid-tone that is visible but not dominant. - -### Slot Count Mismatch - -When your content has fewer items than the template provides: - -**Remove excess elements entirely** -- don't just clear their text. Empty text boxes and orphaned images create visual clutter. - -```bash -# Template has 4 team member cards, you only need 3 -# Remove the 4th card's shape group (check shape indices first) -officecli get template.pptx /slide[4] --depth 1 - -# Remove excess elements (highest index first) -officecli remove template.pptx /slide[4]/shape[8] # 4th member text -officecli remove template.pptx /slide[4]/picture[4] # 4th member photo -officecli remove template.pptx /slide[4]/shape[7] # 4th member name -``` - -When your content has more items than the template: -- Clone the slide and split content across two slides -- Or add new shapes to the existing slide - -### Text Length Mismatch - -- **Shorter replacements**: Usually safe -- **Longer replacements**: May overflow or wrap unexpectedly -- After replacing text, always verify visually with `officecli view template.pptx html --browser` -- Consider truncating or splitting long content to fit the template design - -### Font Cascade from Template Shapes - -**This is the #1 cause of undeliverable template edits.** - -Template shapes (cards, headers, callout boxes) often have font size, font family, and color baked into their text run properties. When you `set --prop text="..."` on these shapes, the NEW text inherits the TEMPLATE's original run formatting, NOT the size/font/color you might expect. - -**Symptom:** Text appears at enormous size (40-60pt), wraps mid-word, or overlaps with other elements. - -**Fix: ALWAYS include explicit size, font, and color when setting text on template shapes:** - -```bash -# BAD -- inherits template's original 48pt display size -officecli set template.pptx "/slide[3]/shape[2]" --prop text="Enterprise" - -# GOOD -- explicit size override -officecli set template.pptx "/slide[3]/shape[2]" --prop text="Enterprise" --prop size=20 --prop font=Arial --prop color=D4A843 -``` - -**Before editing any template shape, inspect it first:** - -```bash -officecli get template.pptx "/slide[3]/shape[2]" -``` - -Look at the reported font size. If it is larger than you want for your content, you MUST override it in your set command. - -**Batch mode:** Include size, font, and color in the props object for every text-setting operation on template shapes: - -```bash -cat <<'EOF' | officecli batch template.pptx -[ - {"command":"set","path":"/slide[3]/shape[2]","props":{"text":"Enterprise","size":"20","font":"Arial","color":"D4A843"}}, - {"command":"set","path":"/slide[3]/shape[3]","props":{"text":"Platform","size":"20","font":"Arial","color":"D4A843"}} -] -EOF -``` - -**If `set --prop size=N` does not visually change the size** (some templates embed formatting at the XML run level that resists high-level overrides), fall back to `raw-set` to directly set the run-level font size: - -```bash -officecli raw-set template.pptx "/slide[3]" \ - --xpath "//p:sp[2]//a:rPr" \ - --action setattr \ - --xml "sz=2000" -``` - -(where `sz=2000` = 20pt in half-points; `sz=2400` = 24pt, etc.) - -### Formatting Preservation - -When setting text with `--prop text="..."`, the first run's formatting is preserved. For mixed formatting, use paragraph and run operations: - -```bash -# Bold header + normal body in same shape -officecli set template.pptx /slide[2]/shape[1]/paragraph[1] --prop bold=true --prop text="Summary" -officecli add template.pptx /slide[2]/shape[1] --type paragraph --prop text="Revenue grew 25% this quarter." --prop bold=false --prop size=16 -``` - ---- - -## Parallel Editing with Subagents - -Slides are independent elements. When editing multiple slides, use subagents for parallel execution. - -**Provide each subagent with:** -- The file path -- Specific slide numbers to edit -- The content/data for those slides -- Design guidelines (fonts, colors, spacing) - -``` -Edit slides 3-5 in /path/to/template.pptx. - -Slide 3: Replace title with "Revenue Overview", update chart data to Q1=42,Q2=58,Q3=65,Q4=78 -Slide 4: Replace team member names and photos (see data below) -Slide 5: Update table with new metrics (see data below) - -Style: font=Calibri, title size=32, body size=16, primary color=1E2761 -Use officecli set/add commands. Do NOT modify structural slide order. -``` - -**Important:** Only use subagents for content editing (step 4). All structural changes (step 3) must be done sequentially in the main thread first. - ---- - -## Template Merge (Placeholder-Based) - -For templates with `{{key}}` placeholders in text: - -```bash -# Merge template with JSON data -officecli merge template.pptx output.pptx --data '{"company":"Acme Corp","quarter":"Q4 2025","revenue":"$5.1M","growth":"25%"}' - -# Or from a JSON file -officecli merge template.pptx output.pptx --data data.json -``` - -This replaces all `{{key}}` placeholders across all slides in a single operation. - ---- - -## Bulk Modifications with Query - -Use CSS-like queries to find and modify elements across the entire presentation: - -```bash -# Find all shapes with Arial font -officecli query template.pptx "shape[font=Arial]" - -# Find all pictures missing alt text -officecli query template.pptx "picture:no-alt" - -# Find shapes containing placeholder text -officecli query template.pptx 'shape:contains("lorem")' - -# Find shapes larger than 15cm wide -officecli query template.pptx "shape[width>=15cm]" -``` - -Then modify found elements by their path: - -```bash -# After finding shape paths from query results, update them -officecli set template.pptx /slide[2]/shape[3] --prop font=Calibri -officecli set template.pptx /slide[4]/picture[1] --prop alt="Product dashboard screenshot" -``` - ---- - -## Raw XML Escape Hatch - -When the high-level CLI cannot express what you need, fall back to raw XML: - -```bash -# View raw XML of a slide -officecli raw template.pptx /slide[1] - -# Modify raw XML -officecli raw-set template.pptx /slide[1] \ - --xpath "//p:sp[1]//a:solidFill/a:srgbClr" \ - --action setattr \ - --xml "val=FF0000" - -# Remove all animations from a slide -officecli raw-set template.pptx /slide[1] \ - --xpath "//p:timing" \ - --action remove -``` - -Raw-set actions: append, prepend, insertbefore, insertafter, replace, remove, setattr. - -XPath namespace prefixes: p (PresentationML), a (DrawingML), r (Relationships), c (Charts). - -**Use raw XML only as a last resort.** The high-level CLI handles most operations correctly without XML knowledge. diff --git a/skills/officecli-track-changes/SKILL.md b/skills/officecli-track-changes/SKILL.md new file mode 100644 index 000000000..3f4eb368b --- /dev/null +++ b/skills/officecli-track-changes/SKILL.md @@ -0,0 +1,132 @@ +--- +name: officecli-track-changes +description: "Track Changes / 修订模式 for .docx files. Use when the user wants to review, audit, redline, or revise a document with tracked changes (修订模式, 审查, redline, revision). MUST load alongside officecli skill." +--- + +# Track Changes / Revision Mode (Word .docx) + +When the user asks to review, audit, redline, or revise a document with tracked changes, use `--prop revision.type=...` / `--prop revision.author=...`. Do NOT use python-docx, unpack/pack, raw-set, or any other approach — officecli v1.0.98+ handles OOXML revision markup natively via the `revision.*` API. + +**API migration**: Previous `trackChange=ins/del/format` has been replaced by `revision.type=ins/del/format`. See the property table below. + +## Preferred: find + revision (Word-style Find & Replace with Track Changes) + +One command does it all — the handler auto-creates paired ins+del markers: + +```bash +# Replace text with tracked change (one command) +officecli set "$FILE" /body --prop find="old text" --prop replace="new text" --prop revision.author=AI + +# Delete text (tracked deletion — no insertion) +officecli set "$FILE" /body --prop find="text to remove" --prop replace= --prop revision.author=AI + +# Format change on matched text (bold all occurrences of "keyword") +officecli set "$FILE" /body --prop find="keyword" --prop bold=true --prop revision.author=AI + +# Regex-based find+replace +officecli set "$FILE" /body --prop 'find=\$\d+' --prop regex=true --prop replace="[PRICE]" --prop revision.author=AI +``` + +## Alternate: Direct run wrapping with `set` + `revision.type` + +```bash +# Mark existing text as inserted +officecli set "$FILE" '/body/p[1]/r[1]' --prop revision.type=ins --prop revision.author=AI + +# Mark existing text as deleted +officecli set "$FILE" '/body/p[1]/r[1]' --prop revision.type=del --prop revision.author=AI + +# Move revision (paired — same revision.id) +officecli set "$FILE" '/body/p[1]/r[1]' --prop revision.type=moveFrom --prop revision.author=AI --prop revision.id=100 +officecli set "$FILE" '/body/p[2]/r[1]' --prop revision.type=moveTo --prop revision.author=AI --prop revision.id=100 +``` + +## Legacy: Split+remove+add workflow (complex multi-change paragraphs) + +```bash +# Step 0: Read run structure FIRST +officecli get "$FILE" '/body/p[N]' --depth 2 + +# Step 1: Isolate target into own run +officecli set "$FILE" '/body/p[N]' --prop find="8" --prop color=auto + +# Step 2: Remove original run +officecli remove "$FILE" '/body/p[N]/r[2]' + +# Step 3: Add ins run +officecli add "$FILE" '/body/p[N]' --type run --after '/body/p[N]/r[1]' --prop revision.type=ins --prop revision.author=AI --prop text="4" + +# Step 4: Add del run +officecli add "$FILE" '/body/p[N]' --type run --after '/body/p[N]/r[1]' --prop revision.type=del --prop revision.author=AI --prop text="8" +``` + +## Revision properties (creation) + +| Property | Values | Where | +|----------|--------|-------| +| `revision.type` | `ins`, `del`, `format`, `moveFrom`, `moveTo` | set on run/paragraph/table/row/cell/section | +| `revision.author` | Any string | paired with `revision.type` | +| `revision.date` | ISO 8601 (optional) | paired with `revision.type` | +| `revision.id` | Integer (auto if omitted) | required for moveFrom/moveTo pairs | + +## Revision actions (accept/reject) + +```bash +# Accept all / reject all +officecli set "$FILE" /revision --prop revision.action=accept +officecli set "$FILE" /revision --prop revision.action=reject + +# Filtered accept/reject +officecli set "$FILE" '/revision[@author=AI]' --prop revision.action=accept +officecli set "$FILE" '/revision[@type=ins]' --prop revision.action=accept +officecli set "$FILE" '/revision[@id=42]' --prop revision.action=accept +``` + +## Verify + +```bash +officecli query "$FILE" revision # list all revisions +officecli query "$FILE" revision --json # JSON output +``` + +## Preview after modifications + +After completing revisions, **show the user a visual preview** so they can see revision marks rendered (red strikethrough, green underline, yellow highlight). Choose the right method for your environment: + +**If your client supports displaying web pages** (browser tool, webview, iframe): +```bash +officecli view "$FILE" html -o /tmp/preview.html +# Then open /tmp/preview.html in your client's browser tool +``` + +**If the user is in AionUI**, the preview panel already shows live updates via `officecli watch` — no action needed. **Do NOT call `view html` in AionUI** (redundant — opens a second tab). + +**If the user is in a CLI-only environment**, fall back to text summary: +```bash +officecli query "$FILE" revision # show revision list as text +``` + +## Contract review pattern + +```bash +FILE="contract_reviewed.docx" +cp original.docx "$FILE" +officecli open "$FILE" + +# One command per change with find + revision +officecli set "$FILE" /body --prop find="3个工作日" --prop replace="5个工作日" --prop revision.author=甲方 +officecli set "$FILE" /body --prop find="8小时" --prop replace="4小时" --prop revision.author=甲方 +officecli set "$FILE" /body --prop find="24小时" --prop replace="12小时" --prop revision.author=甲方 + +officecli query "$FILE" revision +officecli close "$FILE" +``` + +## Critical rules + +1. **Prefer `find + revision`** — one command replaces old→new with ins+del pair. +2. **Use `set` + `revision.type=...`** to wrap existing content — no need to remove/add. +3. **Never fall back to python-docx, unpack/pack, or raw XML** — v1.0.98+ handles all revision types natively. +4. **`revision.type=format` needs a real property change** — pair with `bold=true`, `font.color=...`, etc. +5. **moveFrom/moveTo must share `revision.id`** — same ID on both halves for paired move. +6. **`find` matches within a single run** — if 0 matches, use `get --depth 2` to see boundaries. diff --git a/skills/officecli-word-form/SKILL.md b/skills/officecli-word-form/SKILL.md new file mode 100644 index 000000000..b0da92aac --- /dev/null +++ b/skills/officecli-word-form/SKILL.md @@ -0,0 +1,669 @@ +--- +name: officecli-word-form +description: "Use this skill to create fillable Word forms (.docx) with real Content Controls (SDT) + legacy FormField checkboxes + MERGEFIELD mail-merge placeholders + document protection. Trigger on: 'fillable form', 'form fields', 'content controls', 'SDT', 'word form', 'fill in', 'only editable fields', 'protect document', 'onboarding form', 'HR intake', 'survey template', 'contract / SOW template', 'mail-merge template', 'compliance checklist', 'medical intake questionnaire'. Output is a single .docx where specific fields are editable and the rest is locked. This skill is INDEPENDENT, not a scene layer on docx — payload is `` + `` + `` + `documentProtection`, none of which docx base skill covers. Do NOT trigger for regular reports, letters, memos, academic papers, pitch decks, or any document with no user-fillable fields — route those to officecli-docx or its scene layers." +--- + +# OfficeCLI Word-Form Skill + +**This skill is INDEPENDENT, not a scene layer on docx.** A form's payload — `` controls, `` legacy fields, `` mail-merge, `documentProtection` — is a distinct element class from docx's paragraph/heading/style primitives. Its QA is different too: docx's Delivery Gate cares about visual layout and live PAGE fields, this skill's cares about data plumbing (protection enforced / alias+tag / items injected / name ≤ 20 / no underscore anti-pattern). **Reverse handoff:** if the user's document has no fillable fields (report, letter, memo, thesis, proposal), route to `officecli-docx` or a docx scene skill — don't use this one. + +## BEFORE YOU START (CRITICAL) + +**If `officecli` is not installed:** + +`macOS / Linux` + +```bash +if ! command -v officecli >/dev/null 2>&1; then + curl -fsSL https://d.officecli.ai/install.sh | bash +fi +``` + +`Windows (PowerShell)` + +```powershell +if (-not (Get-Command officecli -ErrorAction SilentlyContinue)) { + irm https://d.officecli.ai/install.ps1 | iex +} +``` + +Verify: `officecli --version` + +If `officecli` is still not found after first install, open a new terminal and run the verify command again. + +If the install command above fails (e.g. blocked by security policy, no network access, or insufficient permissions), install manually — download the binary for your platform from https://github.com/iOfficeAI/OfficeCLI/releases — then re-run the verify command. + +## Help-First Rule + +This skill teaches what a real form needs, not every CLI flag. When a prop / alias / enum is uncertain, consult help BEFORE guessing: `officecli help docx [element] [--json]` (e.g. `sdt`, `formfield`, `field`). Help is pinned to installed version — when this skill and help disagree, **help wins**. Every `--prop X=` below was verified against `officecli help docx ` on v1.0.63. + +## Mental Model & Inheritance + +A Word form is a `.docx` plus four OpenXML payload layers plain-docx skills do not touch: **``** content controls (5 types: text / richtext / dropdown / combobox / date), **``** legacy FormField (ONLY way to get a real checkbox on v1.0.63), **``** complex fields (MERGEFIELD, REF, PAGEREF, SEQ, IF — template-time, not user-fill), and **`documentProtection`** (the lock that makes non-field text read-only in Word). + +**No inheritance from docx v2.** docx's Delivery Gate (cover-fill %, live-PAGE check) does NOT apply — form QA is `view forms` + `query sdt alias+tag` + `protectionEnforced`. + +**Reverse handoff to docx.** Route back to `officecli-docx` for reports / letters / memos / thesis / pitch decks / any document with no editable fields. Use **this** skill when the document's purpose is data capture or template merge. + +## Shell & Execution Discipline + +**One command at a time. Read output before the next.** OfficeCLI is incremental — every `add` / `set` / `remove` immediately mutates the file. All recipes below use `FILE=form.docx` as a shell variable. + +**Three shell-escape layers:** + +1. **Quote every path with `[N]`** — zsh/bash glob-expand brackets. `officecli get "$FILE" /body/sdt[1]` fails with `no matches found`. Correct: `officecli get "$FILE" '/body/sdt[1]'`. +2. **Single-quote any prop containing `$`** — `"Total: $50,000"` becomes `"Total: ,000"` after `$50` variable expansion. Correct: `'Total: $50,000'`. +3. **`--after find:` uses outer single quotes, never inner double quotes** — `--after find:"Client Signature:"` makes the quotes part of the search string; match fails. Correct: `--after 'find:Client Signature:'`. + +**`WARNING: UNSUPPORTED` (exit 2) is a silently-wrong element.** The CLI created the element *without* the rejected prop — dropdown with no items, date with default format, SDT with no lock. Any UNSUPPORTED in your build log means your command was wrong: stop, rewrite to Path B (raw-set) or a separate `set`. Do not ship on top. + +**`protection=forms` is the LAST command.** Not CLI-enforced — `add` / `set` / `raw-set` still run under any protection mode — but finishing with protection gives Word users a consistent locked experience on first open. + +### `--after find:` micro-playbook + +`--after find:` matches the **first** occurrence. Bad anchor = wrong insertion location, expensive to debug. Three rules: + +1. **Anchor must be globally unique.** In bilingual contracts "甲方签字" matches both parties — use a unique phrase like "甲方签字(Service Provider)" or full English title. +2. **After insert, `/body/p[last()]` is unreliable** — the find insertion changes `` child order. To continue operating on the new paragraph, read its real paraId: `officecli query "$FILE" paragraph --json | jq -r '.data.results[-1].format.paraId'`. +3. **Chinese + full-width parens `()`** match literally in `find`, but when unsure, `officecli view "$FILE" text | grep -n "锚点"` first to confirm the exact bytes in the file. + +```bash +# Trap: first-match hits 甲方 only, 乙方 missed +officecli add "$FILE" /body --type sdt --after 'find:签字' + +# Fix: two signatories, two unique anchors +officecli add "$FILE" /body --type sdt --prop alias=Party_A_Name --prop tag=party_a \ + --after 'find:甲方签字(Service Provider)' +PID_A=$(officecli query "$FILE" paragraph --json | jq -r '.data.results[-1].format.paraId') +officecli add "$FILE" "/body/p[@paraId='$PID_A']" --type sdt --prop alias=Party_A_Title --prop tag=party_a_title +``` + +Inline SDT via `--after find:` is added as a child of the matched paragraph, not as a new paragraph — use this when label + SDT must share a line. + +## What makes a real form (identity) + +A real fillable form requires **structured fields** + **document protection**. + +| Approach | Word user sees | CLI-readable | Real form? | +|---|---|---|---| +| SDT controls + `protection=forms` | Gray-bordered fields; rest locked | `query sdt` / `view forms` | **YES** | +| FormField checkbox + `protection=forms` | Real clickable checkbox; rest locked | `query formfield` / `view forms` | **YES** (checkbox only) | +| MERGEFIELD placeholders | `«CustomerName»` merged by downstream engine | `query field` | **YES** (template-time) | +| Underscores `___` / blank lines | Visual-only; whole doc editable | No — no structured fields | **NO** | + +**Do not simulate fields with underscores.** `姓名:_______________` produces zero structured data and leaks past every verification. Always use `--type sdt` or `--type formfield`. + +**Checkbox is formfield, NOT SDT.** `--type sdt --prop type=checkbox` exits 1 (`SDT type 'checkbox' is not implemented`). Every checkbox in every recipe uses `--type formfield --prop type=checkbox`. + +**MERGEFIELD is a separate track.** `view forms` lists SDT + formfield only; `query field` lists complex fields only. Two disjoint inventories; both valid in one file. + +## Requirements for Outputs (hard floor) + +Every form must satisfy these — Delivery Gate enforces each as an executable check. + +1. `protection=forms` enforced (`get $FILE /` → `protectionEnforced=True`). +2. Every SDT has both `alias` + `tag`. +3. Every dropdown/combobox has non-empty `items=...` in `view forms`. +4. Every date SDT shows the intended `format=...`. +5. Every locked SDT shows `lock=sdtLocked` / `contentLocked` / `sdtContentLocked` as intended. +6. Zero `WARNING: UNSUPPORTED` in build log. +7. Zero `type=checkbox` on any SDT. +8. Every formfield `name` ≤ 20 characters. +9. Zero underscore-line / blank-line placeholders. +10. Field types match user intent (short text / paragraph / fixed list / list+custom / date / boolean). + +## Three Paths (core decision) + +CLI v1.0.63 exposes exactly **four canonical props** on SDT: `{type, tag, alias, text}`. Everything else — `items`, `format`, `lock`, `placeholder`, `name`, `maxlength` — is UNSUPPORTED at add-time and silently discarded. The skill therefore splits every SDT need into three paths. **Pick the path before writing a single command.** + +### Path A — Pure CLI (simple forms) + +**Use when**: the field only needs a label, an initial text, and a type. Acceptable if dropdown/combobox items can be empty at first and dates can default to `yyyy-MM-dd`. + +```bash +officecli add "$FILE" /body --type sdt \ + --prop type=text \ + --prop alias="Full Name" --prop tag=full_name \ + --prop text="Enter full name" +# Canonical follow-ups (not on add): +# officecli set "$FILE" '/body/sdt[N]' --prop lock=sdtlocked +# officecli set "$FILE" / --prop protection=forms +``` + +### Path B — CLI + `raw-set` bridge (complex attrs) + +**Use when**: dropdown/combobox needs options, or date needs a non-default format. `raw-set` is OfficeCLI's universal OpenXML fallback — `officecli --help` lists it as a top-level command. + +```bash +# Step 1 — Path A skeleton (generates automatically) +officecli add "$FILE" /body --type sdt \ + --prop type=dropdown --prop alias="Department" --prop tag=dept + +# Step 2 — raw-set injects s +officecli raw-set "$FILE" /document \ + --xpath "//w:sdt[w:sdtPr/w:tag/@w:val='dept']/w:sdtPr/w:dropDownList" \ + --action append \ + --xml '' +``` + +### Path C — Word template (beyond raw-set) + +**Use when**: `picture` SDT (signature image), real SDT checkbox (`type=checkbox` exits 1), `placeholderDocPart` prompt text, grouped SDTs wrapping multiple paragraphs, or custom richtext appearance. These involve cross-part relationships or nesting beyond `--prop` reach. + +```bash +# One-time in Word: Developer tab → Insert Content Control → Save as template.docx +cp templates/onboarding_with_signature.docx "$FILE" +officecli open "$FILE" +officecli view "$FILE" forms # inspect embedded controls + paths +officecli set "$FILE" '/body/sdt[@sdtId=3]' --prop text="Jane Smith" +officecli set "$FILE" / --prop protection=forms +``` + +### Decision table + +| Need | Path | Note | +|---|---|---| +| text / richtext SDT with default string | **A** | four canonical props cover it | +| text SDT that must be locked | **A + set lock** | `lock` only takes effect via `set`, not `add` | +| dropdown / combobox **with options** | **B** | raw-set append `` | +| date SDT with non-default format | **B** | raw-set setattr `w:dateFormat/@w:val` | +| real checkbox | **FormField** | `--type formfield --prop type=checkbox` (see §Legacy FormField) | +| mail-merge placeholder | **MERGEFIELD** | `--type field --prop fieldType=mergefield` (see §MERGEFIELD) | +| signature picture, grouped SDT, placeholder part | **C** | build skeleton in Word, fill via CLI | + +## Quick Start — Path A + FormField (minimal intake form) + +Two SDT text fields, one checkbox, protection. Paste and adapt; this is the smallest form worth shipping. + +```bash +FILE=intake.docx +officecli close "$FILE" 2>/dev/null; rm -f "$FILE" # preflight: clear stale resident / prior file (cold-start after CLI upgrade commonly leaks a resident) +officecli create "$FILE" +officecli open "$FILE" + +officecli set "$FILE" / --prop title="Employee Onboarding Intake" \ + --prop docDefaults.font="Calibri" --prop docDefaults.fontSize="12pt" + +officecli add "$FILE" /body --type paragraph \ + --prop text="Employee Onboarding Intake" --prop style=Heading1 \ + --prop size=20 --prop bold=true --prop spaceAfter=18pt + +officecli add "$FILE" /body --type paragraph \ + --prop text="Full Name:" --prop size=11 --prop bold=true --prop spaceAfter=4pt +officecli add "$FILE" /body --type sdt --prop type=text \ + --prop alias="Full Name" --prop tag=full_name --prop text="Enter full name" + +officecli add "$FILE" /body --type paragraph \ + --prop text="Start Date:" --prop size=11 --prop bold=true --prop spaceAfter=4pt +officecli add "$FILE" /body --type sdt --prop type=date \ + --prop alias="Start Date" --prop tag=start_date + +officecli add "$FILE" /body --type paragraph \ + --prop text="Read and agree to employee handbook" --prop size=11 --prop spaceAfter=4pt +officecli add "$FILE" /body --type formfield \ + --prop type=checkbox --prop name=agree_handbook --prop checked=false + +officecli set "$FILE" '/body/sdt[1]' --prop lock=sdtlocked +officecli set "$FILE" '/body/sdt[2]' --prop lock=sdtlocked +officecli set "$FILE" / --prop protection=forms +officecli close "$FILE" +officecli view "$FILE" forms +``` + +## Path B — raw-set recipes + +Three recipes cover almost every complex-attr need on SDT forms. + +### B1 — Dropdown items (append) + +```bash +# Skeleton (Path A) +officecli add "$FILE" /body --type sdt --prop type=dropdown \ + --prop alias="Department" --prop tag=dept + +# Inject items +officecli raw-set "$FILE" /document \ + --xpath "//w:sdt[w:sdtPr/w:tag/@w:val='dept']/w:sdtPr/w:dropDownList" \ + --action append \ + --xml '' + +# Verify +officecli get "$FILE" '/body/sdt[1]' # expect: type=dropdown items=Engineering,Finance,HR +``` + +**Template.** Swap `` / `